From 152031815b0425d9e56bd018caeb64867de51c1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Sep 2024 17:44:39 +0000 Subject: [PATCH 01/17] Bump System.Text.Json Bumps the nuget group with 1 update in the /archive/Tests directory: [System.Text.Json](https://github.com/dotnet/runtime). Updates `System.Text.Json` from 8.0.0-preview.3.23174.8 to 8.0.4 - [Release notes](https://github.com/dotnet/runtime/releases) - [Commits](https://github.com/dotnet/runtime/compare/v8.0.0-preview.3.23174.8...v8.0.4) --- updated-dependencies: - dependency-name: System.Text.Json dependency-type: direct:production dependency-group: nuget ... Signed-off-by: dependabot[bot] --- archive/Tests/Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/archive/Tests/Tests.csproj b/archive/Tests/Tests.csproj index 41673a8b..13369b4e 100644 --- a/archive/Tests/Tests.csproj +++ b/archive/Tests/Tests.csproj @@ -28,7 +28,7 @@ - + From e4f718a3652d245e0bce011971d31fb26ef20664 Mon Sep 17 00:00:00 2001 From: Miha Kralj Date: Fri, 25 Oct 2024 22:21:02 -0700 Subject: [PATCH 02/17] Cleanup of indicators --- docs/indicators/indicators.md | 212 +++++++++++++------------ docs/styles.css | 2 +- lib/{volatility => oscillators}/Cmo.cs | 3 - 3 files changed, 114 insertions(+), 103 deletions(-) rename lib/{volatility => oscillators}/Cmo.cs (96%) diff --git a/docs/indicators/indicators.md b/docs/indicators/indicators.md index 4ef85e51..cb11589d 100644 --- a/docs/indicators/indicators.md +++ b/docs/indicators/indicators.md @@ -4,39 +4,106 @@ ✔️= Validation tests passed
❌= Issue -|**VOLATILITY INDICATORS**|QuanTALib|Skender.Stock|TALib.NETCore| +|**MOMENTUM INDICATORS**|**QuanTALib**|Skender.Stock|TALib.NETCore| |--|:--:|:--:|:--:| +|*DMI - Directional Movement Index|`?`|GetDmi|| +|*DMX - Jurik Directional Movement Index|`?`||| +|*MOM - Momentum|`?`||| +|*VEL - Jurik Signal Velocity|`?`||| +|ADX - Average Directional Movement Index|`?`|GetAdx|Adx| +|ADXR - Average Directional Movement Index|`?`|Rating|Adxr| +|APO - Absolute Price Oscillator|`?`|Apo|| +|DPO - Detrended Price Oscillator|`?`|GetDpo|| +|MACD - Movign Average Convergence/Divergence|`?`||| +|PO - Price Oscillator|`?`||| +|PPO - Percentage Price Oscillator|`?`||| +|PMO - Price Momentum Oscillator|`?`|GetPmo|| +|PRS - Price Relative Strength|`?`|GetPrs|| +|ROC - Rate of Change|`?`|GetRoc|| +|TRIX - 1-day ROC of TEMA|`?`|GetTrix|| +|VORTEX - Vortex Indicator|`?`||| +
+|**VOLATILITY INDICATORS**|**QuanTALib**|Skender.Stock|TALib.NETCore| +|ADR - Average Daily Range|||| +|ANDREW - Andrew's Pitchfork|||| +|ATR - Average True Range|`Atr`|GetAtr|Atr| +|ATRP - Average True Range Percent|||| +|ATRSTOP - ATR Trailing Stop ||GetAtrStop|| +|BBANDS - Bollinger Bands®||BollingerBands|| +|CHAND - Chandelier Exit||GetChandelier|| +|CVI - Chaikins Volatility|||| +|DON - Donchian Channels||GetDonchian|| +|FCB - Fractal Chaos Bands||GetFcb|| +|HV - Historical Volatility|||| +|ICH - Ichimoku Cloud||GetIchimoku|| +|KEL - Keltner Channels||GetKeltner|| +|NATR - Normalized Average True Range||GetAtr|| +|CHN - Price Channel Indicator|||| +|SAR - Parabolic Stop and Reverse||GetParabolicSar|| +|STARC - Starc Bands||GetStarcBands|| +|TR - True Range|||| +|UI - Ulcer Index||GetUlcerIndex|| +|VSTOP - Volatility Stop||GetVolatilityStop|| +
+|**OSCILLATORS**|**QuanTALib**|Skender.Stock|TALib.NETCore| +|RSI - Relative Strength Index|`Rsi`|GetRsi|| +|RSX - Jurik Trend Strength Index|`Rsx`||| +|AC - Acceleration Oscillator||||| +|AO - Awesome Oscillator||GetAwesome||| +|AROON - Aroon oscillator||GetAroon|Aroon|| +|BOP - Balance of Power||GetBop|Bop|| +|CCI - Commodity Channel Index||GetCci|Cci|| +|CFO - Chande Forcast Oscillator||||| +|CMO - Chande Momentum Oscillator||GetCmo|Cmo|| +|CHOP - Choppiness Index||GetChop||| +|COG - Ehler's Center of Gravity||||| +|COPPOCK - Coppock Curve||||| +|CRSI - Connor RSI||GetConnorsRsi||| +|CTI - Ehler's Correlation Trend Indicator||||| +|DOSC - Derivative Oscillator||||| +|EFI - Elder Ray's Force Index||GetElderRay||| +|FISHER - Fisher Transform||||| +|FOSC - Forecast Oscillator|||||| +|GATOR - Williams Alliator Oscillator||GetGator||| +|KDJ - KDJ Indicator (trend reversal)||||| +|KRI - Kairi Relative Index||||| +|RVGI - Relative Vigor Index||||| +|SMI - Stochastic Momentum Index||GetSmi||| +|SRSI - Stochastic RSI||GetStochRsi||| +|STC - Schaff Trend Cycle||GetStc||| +|STOCH - Stochastic Oscillator||`GetStoch||| +|TSI - True Strength Index||GetTsi||| +|UO - Ultimate Oscillator||GetUltimate||| +|WILLR - Larry Williams' %R||GetWilliamsR||| +
+|**VOLUME INDICATORS**|**QuanTALib**|Skender.Stock|TALib.NETCore| |ADL - Chaikin Accumulation Distribution Line||GetAdl|Ad|| |ADOSC - Chaikin Accumulation Distribution Oscillator||GetChaikinOsc|AdOsc|| -|ATR - Average True Range|`Atr`|GetAtr|Atr|| -|ATRP - Average True Range Percent||||| -|ATRSTOP - ATR Trailing Stop ||GetAtrStop||| -|BBANDS - Bollinger Bands®||BollingerBands||| -|CHAND - Chandelier Exit||GetChandelier||| -|CRSI - Connor RSI||GetConnorsRsi||| -|CVI - Chaikins Volatility||||| -|DON - Donchian Channels||GetDonchian||| -|FCB - Fractal Chaos Bands||GetFcb||| -|FISHER - Fisher Transform||||| -|HV - Historical Volatility||||| -|ICH - Ichimoku Cloud||GetIchimoku||| -|KEL - Keltner Channels||GetKeltner||| -|NATR - Normalized Average True Range||GetAtr||| -|CHN - Price Channel Indicator||||| -|RSI - Relative Strength Index|`Rsi`|GetRsi||| -|SAR - Parabolic Stop and Reverse||GetParabolicSar||| -|SRSI - Stochastic RSI||GetStochRsi||| -|STARC - Starc Bands||GetStarcBands||| -|TR - True Range||||| -|UI - Ulcer Index||GetUlcerIndex||| -|VSTOP - Volatility Stop||GetVolatilityStop||| -|**NUMERICAL ANALYSIS**|QuanTALib|Skender.Stock|TALib.NETCore| +|AOBV - Archer On-Balance Volume||||| +|CMF - Chaikin Money Flow||GetCmf||| +|EOM - Ease of Movement||||| +|KVO - Klinger Volume Oscillator||GetKvo|||| +|MFI - Money Flow Index||GetMfi||| +|NVI - Negative Volume Index||||| +|OBV - On-Balance Volume||GetObv||| +|PVI - Positive Volume Index||||| +|PVOL - Price-Volume||||| +|PVO - Percentage Volume Oscillator||GetPvo||| +|PVR - Price Volume Rank||||| +|PVT - Price Volume Trend||||| +|TVI - Trade Volume Index||||| +|VP - Volume Profile||||| +|VWAP - Volume Weighted Average Price||GetVwap||| +|VWMA - Volume Weighted Moving Average||GetVwma|||| +
+|**NUMERICAL ANALYSIS**|**QuanTALib**|Skender.Stock|TALib.NETCore| |BETA - Beta coefficient||||| |CORR - Correlation Coefficient||||| |CURVATURE - Rate of Change in Direction or Slope|`Curvature`|||| |ENTROPY - Measure of Uncertainty or Disorder|`Entropy`|||| |KURTOSIS - Measure of Tails/Peakedness|`Kurtosis`|||| |HUBER - Huber Loss|`Huberloss`|||| +|HURST - Hurst Exponent||GetHurst||| |MAX - Maximum with exponential decay|`Max`|||| |MEDIAN - Middle value|`Median`|||| |MIN - Minimum with exponential decay|`Min`|||| @@ -47,9 +114,11 @@ |SLOPE - Rate of Change, Linear Regression|`Slope`|||| |STDDEV - Standard Deviation, Measure of Spread|`Stddev`|||| |THEIL - Theil's U Statistics||||| +|TSF - Time Series Forecast|||`✔️`|`✔️`| |VARIANCE - Average of Squared Deviations|`Variance`|||| |ZSCORE - Standardized Score|`Zscore`|||| -|**ERRORS**|QuanTALib|Skender.Stock|TALib.NETCore| +
+|**ERRORS**|**QuanTALib**|Skender.Stock|TALib.NETCore| |MAE - Mean Absolute Error|`Mae`|||| |MAPD - Mean Absolute Percentage Deviation|`Mapd`|||| |MAPE - Mean Absolute Percentage Error|`Mape`|||| @@ -64,13 +133,14 @@ |RSE - Relateive Squared Error|`Rse`|||| |RMSLE - Root Mean Squared Logarithmic Error|`Rmsle`|||| |SMAPE - Symmetric Mean Absolute Percentage Error|`Smape`|||| -|**AVERAGES & TRENDS**QuanTALib|Skender.Stock|TALib.NETCore| +
+|**AVERAGES & TRENDS**|**QuanTALib**|Skender.Stock|TALib.NETCore| |AFIRMA - Autoregressive Finite Impulse Response Moving Average|`Afirma`|||| -|ALMA - Arnaud Legoux Moving Average|`Alma`|`✔️`||| -|⭐DEMA - Double EMA Average|`Dema`|`⭐`|`⭐`|`⭐`|| +|ALMA - Arnaud Legoux Moving Average|`Alma`|`✔️`|| +|DEMA - Double EMA Average|`Dema`|`✔️`|`✔️`| |DSMA - Deviation Scaled Moving Average|`Dsma`|||| |DWMA - Double WMA Average|`Dwma`|||| -|⭐EMA - Exponential Moving Average|`Ema`|`⭐`|`⭐`|`⭐`|`⭐`| +|EMA - Exponential Moving Average|`Ema`|`⭐`|`⭐`| |EPMA - Endpoint Moving Average|`Epma`|`✔️`||| |FRAMA - Fractal Adaptive Moving Average|`Frama`|||| |FWMA - Fibonacci Weighted Moving Average|`Fwma`|||| @@ -80,87 +150,31 @@ |HMA - Hull Moving Average|`Hma`|`✔️`||`✔️`| |HWMA - Holt-Winter Moving Average|`Hwma`|||| |JMA - Jurik Moving Average|`Jma`|||| +|JORDAN - Jordan Moving Average||||| |KAMA - Kaufman's Adaptive Moving Average|`Kama`|`✔️`|`✔️`|`✔️`| -|KDJ - KDJ Indicator (trend reversal)||||| |LTMA - Laguerre Transform Moving Average|`Ltma`|||| |MAAF - Median-Average Adaptive Filter|`Maaf`|||| |MAMA - MESA Adaptive Moving Average|`Mama`|`✔️`|`✔️`|| |MGDI - McGinley Dynamic Indicator|`Mgdi`|`✔️`||| +|MLMA - Minimal Lag Moving Average||||| |MMA - Modified Moving Average|`Mma`|||| |PPMA - Pivot Point Moving Average||||| |PWMA - Pascal's Weighted Moving Average|`Pwma`|||| |QEMA - Quad Exponential Moving Average|`Qema`|||| |RMA - WildeR's Moving Average|`Rma`|||| |SINEMA - Sine Weighted Moving Average|`Sinema`|||| -|⭐SMA - Simple Moving Average|`Sma`|`⭐`|`⭐`|`⭐`|`⭐`| -|SMMA - Smoothed Moving Average|`Smma`|`✔️`||| -|SSF - Ehler's Super Smoother Filter||||| -|SUPERTREND - Supertrend||`✔️`||| -|T3 - Tillson T3 Moving Average|`T3`|`✔️`|`✔️`|| -|TEMA - Triple EMA Average|`Tema`|`✔️`|`✔️`|`✔️`| -|TRIMA - Triangular Moving Average|`Trima`|`✔️`||`✔️`| -|TSF - Time Series Forecast|||`✔️`|`✔️`| -|VIDYA - Variable Index Dynamic Average|`Vidya`|||`✔️`| -|VORTEX - Vortex Indicator||`✔️`||| -|WMA - Weighted Moving Average|`Wma`|`✔️`||`✔️`| -|ZLEMA - Zero Lag EMA Average|`Zlema`|||`✔️`| - -|**MOMENTUM INDICATORS & OSCILLATORS**|QuanTALib|Skender.Stock|TALib.NETCore| -|AC - Acceleration Oscillator||||| -|ADX - Average Directional Movement Index||GetAdx|Adx|| -|ADXR - Average Directional Movement Index||Rating|Adxr|| -|AO - Awesome Oscillator||GetAwesome||| -|APO - Absolute Price Oscillator||Apo||| -|AROON - Aroon oscillator||GetAroon|Aroon|| -|BOP - Balance of Power||GetBop|Bop|| -|CCI - Commodity Channel Index||GetCci|Cci|| -|CFO - Chande Forcast Oscillator||||| -|CMO - Chande Momentum Oscillator||GetCmo|Cmo|| -|CHOP - Choppiness Index||GetChop||| -|COG - Center of Gravity||||| -|COPPOCK - Coppock Curve||||| -|CTI - Ehler's Correlation Trend Indicator||||| -|DPO - Detrended Price Oscillator||GetDpo||| -|DMI - Directional Movement Index||GetDmi||| -|EFI - Elder Ray's Force Index||GetElderRay||| -|FOSC - Forecast oscillator|||||| -|GATOR - Gator oscillator||GetGator||| -|HURST - Hurst Exponent||GetHurst||| -|KRI - Kairi Relative Index||||| -|KVO - Klinger Volume Oscillator||GetKvo|||| -|MACD - Movign Average Convergence/Divergence||`✔️`|`✔️`|| -|MFI - Money Flow Index||GetMfi||| -|MOM - Momentum||||| -|NVI - Negative Volume Index||||| -|PO - Price Oscillator||||| -|PPO - Percentage Price Oscillator||||| -|PMO - Price Momentum Oscillator||GetPmo||| -|PVI - Positive Volume Index||||| -|ROC - Rate of Change||GetRoc||| -|RVGI - Relative Vigor Index||||| -|SMI - Stochastic Momentum Index||GetSmi||| -|STC - Schaff Trend Cycle||GetStc||| -|STOCH - Stochastic Oscillator||`GetStoch||| -|TRIX - 1-day ROC of TEMA||GetTrix||trix.Run| -|TSI - True Strength Index||GetTsi||| -|UO - Ultimate Oscillator||GetUltimate||| -|WILLR - Larry Williams' %R||GetWilliamsR||| -|WGAT - Williams Alligator||GetAlligator||| -|**VOLUME INDICATORS**|QuanTALib|Skender.Stock|TALib.NETCore| -|AOBV - Archer On-Balance Volume||||| -|CMF - Chaikin Money Flow||GetCmf||| -|EOM - Ease of Movement||||| -|KVO - Klinger Volume Oscilaltor||||| -|OBV - On-Balance Volume||GetObv||| -|PRS - Price Relative Strength||`GetPrs||| -|PVOL - Price-Volume||||| -|PVO - Percentage Volume Oscillator||GetPvo||| -|PVR - Price Volume Rank||||| -|PVT - Price Volume Trend||||| -|VP - Volume Profile||||| -|VWAP - Volume Weighted Average Price||GetVwap||| -|VWMA - Volume Weighted Moving Average||GetVwma|||| -|**BASIC TRANSFORMS**|QuanTALib|Skender.Stock|TALib.NETCore| +|SMA - Simple Moving Average|`Sma`||| +|SMMA - Smoothed Moving Average|`Smma`|`✔️`|| +|SSF - Ehler's Super Smoother Filter|||| +|SUPERTREND - Supertrend||`✔️`|| +|T3 - Tillson T3 Moving Average|`T3`|`✔️`|`✔️`| +|TEMA - Triple EMA Average|`Tema`|`✔️`|`✔️`| +|TRIMA - Triangular Moving Average|`Trima`|`✔️`|| +|VIDYA - Variable Index Dynamic Average|`Vidya`||| +|WMA - Weighted Moving Average|`Wma`|`✔️`|| +|ZLEMA - Zero Lag EMA Average|`Zlema`||| +
+|**BASIC TRANSFORMS**|**QuanTALib**|Skender.Stock|TALib.NETCore| |OC2 - Midpoint price|️`.OC2`|CandlePart.OC2|MidPoint| |HL2 - Median Price|️`.HL2`|CandlePart.HL2|MedPrice| |HLC3 - Typical Price|️`.HLC3`|CandlePart.HLC3|TypPrice| diff --git a/docs/styles.css b/docs/styles.css index 143773fa..5f30ab96 100644 --- a/docs/styles.css +++ b/docs/styles.css @@ -97,7 +97,7 @@ --table-row-odd-background: var(--mono-shade2); /* Layout */ - --content-max-width: 100em; + --content-max-width: 75em; /* Cover */ --cover-margin: 0 auto; diff --git a/lib/volatility/Cmo.cs b/lib/oscillators/Cmo.cs similarity index 96% rename from lib/volatility/Cmo.cs rename to lib/oscillators/Cmo.cs index df64d3a9..0b8d92c5 100644 --- a/lib/volatility/Cmo.cs +++ b/lib/oscillators/Cmo.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; - namespace QuanTAlib; /// From c21b96152ce10aa264375e94df8e1648d39f7a0b Mon Sep 17 00:00:00 2001 From: Miha Date: Sat, 26 Oct 2024 23:54:55 -0700 Subject: [PATCH 03/17] macos dev update --- Directory.Build.props | 4 +- Tests/test_eventing.cs | 8 +- Tests/test_updates_errors.cs | 2 +- Tests/test_updates_volatility.cs | 4 +- docs/_sidebar.md | 3 + docs/indicators/averages/jma/calc.md | 48 ++++ docs/indicators/indicators.md | 304 ++++++++++---------- lib/averages/_list.md | 33 +++ lib/errors/{Huberloss.cs => Huber.cs} | 6 +- lib/errors/_list.md | 16 ++ lib/momentum/_list.md | 16 ++ lib/oscillators/Cmo.cs | 13 +- lib/{volatility => oscillators}/Rsi.cs | 14 +- lib/{volatility => oscillators}/Rsx.cs | 15 +- lib/oscillators/_list.md | 29 ++ lib/statistics/_list.md | 20 ++ lib/volatility/{Historical.cs => Hv.cs} | 6 +- lib/volatility/{Realized.cs => Rv.cs} | 18 +- lib/volatility/_list.md | 34 +++ lib/volume/_list.md | 18 ++ notebooks/charting.dib | 10 +- notebooks/jma.dib | 160 +---------- quantower/Averages/ZlemaIndicator.cs | 2 +- quantower/Averages/_Averages.csproj | 6 +- quantower/Statistics/_Statistics.csproj | 3 +- quantower/Volatility/HistoricalIndicator.cs | 4 +- quantower/Volatility/RealizedIndicator.cs | 4 +- quantower/Volatility/_Volatility.csproj | 3 +- 28 files changed, 458 insertions(+), 345 deletions(-) create mode 100644 docs/indicators/averages/jma/calc.md create mode 100644 lib/averages/_list.md rename lib/errors/{Huberloss.cs => Huber.cs} (92%) create mode 100644 lib/errors/_list.md create mode 100644 lib/momentum/_list.md rename lib/{volatility => oscillators}/Rsi.cs (76%) rename lib/{volatility => oscillators}/Rsx.cs (70%) create mode 100644 lib/oscillators/_list.md create mode 100644 lib/statistics/_list.md rename lib/volatility/{Historical.cs => Hv.cs} (95%) rename lib/volatility/{Realized.cs => Rv.cs} (85%) create mode 100644 lib/volatility/_list.md create mode 100644 lib/volume/_list.md diff --git a/Directory.Build.props b/Directory.Build.props index df85d4db..311a99e7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -51,9 +51,7 @@ - - - + D:\Quantower $([System.IO.Directory]::GetDirectories("$(QuantowerRoot)\TradingPlatform", "v1*")[0]) diff --git a/Tests/test_eventing.cs b/Tests/test_eventing.cs index 9a61defd..57f519ca 100644 --- a/Tests/test_eventing.cs +++ b/Tests/test_eventing.cs @@ -51,6 +51,12 @@ public class EventingTests ("Tema", new Tema(p), new Tema(input, p)), ("Kama", new Kama(2, 30, 6), new Kama(input, 2, 30, 6)), ("Zlema", new Zlema(p), new Zlema(input, p)), + // oscillators + ("Rsi", new Rsi(p), new Rsi(input, p)), + ("Rsx", new Rsx(p), new Rsx(input, p)), + ("Cmo", new Cmo(p), new Cmo(input, p)), + // volatility + ("Rv", new Rv(p), new Rv(input, p)), // error classes ("Mae", new Mae(p), new Mae(input, p)), ("Mapd", new Mapd(p), new Mapd(input, p)), @@ -67,7 +73,7 @@ public class EventingTests ("Rse", new Rse(p), new Rse(input, p)), ("Smape", new Smape(p), new Smape(input, p)), ("Rsquared", new Rsquared(p), new Rsquared(input, p)), - ("Huberloss", new Huberloss(p), new Huberloss(input, p)) + ("Huber", new Huber(p), new Huber(input, p)) }; // Generate 200 random values and feed them to both direct and event-based indicators diff --git a/Tests/test_updates_errors.cs b/Tests/test_updates_errors.cs index 67db6037..13641eb8 100644 --- a/Tests/test_updates_errors.cs +++ b/Tests/test_updates_errors.cs @@ -20,7 +20,7 @@ public class UpdateTests [Fact] public void Huberloss_Update() { - var indicator = new Huberloss(period: 14); + var indicator = new Huber(period: 14); double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); for (int i = 0; i < RandomUpdates; i++) diff --git a/Tests/test_updates_volatility.cs b/Tests/test_updates_volatility.cs index ba7976ee..5a041c26 100644 --- a/Tests/test_updates_volatility.cs +++ b/Tests/test_updates_volatility.cs @@ -45,7 +45,7 @@ public class VolatilityUpdateTests [Fact] public void Historical_Update() { - var indicator = new Historical(period: 14); + var indicator = new Hv(period: 14); double initialValue = indicator.Calc(new TBar(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true)); for (int i = 0; i < RandomUpdates; i++) @@ -60,7 +60,7 @@ public class VolatilityUpdateTests [Fact] public void Realized_Update() { - var indicator = new Realized(period: 14); + var indicator = new Rv(period: 14); double initialValue = indicator.Calc(new TBar(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true)); for (int i = 0; i < RandomUpdates; i++) diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 5df2cf71..1d6e0c3e 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -1,5 +1,8 @@ * [Home](/) + +[JMA](indicators/averages/jma/calc.md) + * Introduction * [Overview]() * [Features]() diff --git a/docs/indicators/averages/jma/calc.md b/docs/indicators/averages/jma/calc.md new file mode 100644 index 00000000..86c9c3f4 --- /dev/null +++ b/docs/indicators/averages/jma/calc.md @@ -0,0 +1,48 @@ +# JMA Calculation + +### Initial Parameters: + +$\beta = factor \cdot \frac{period - 1}{factor \cdot (period - 1) + 2}$ + +$len1 = \frac{\ln(\sqrt{period - 1})}{\ln(2)} + 2$ + +$pow1 = \max(len1 - 2, 0.5)$ + +$phase \in [0.5, 2.5]$ (clamped to $(phase \cdot 0.01) + 1.5$) + +### Volatility Calculations: + +$del1_t = price_t - upperBand_{t-1}$ + +$del2_t = price_t - lowerBand_{t-1}$ + +$volty_t = \max(|del1_t|, |del2_t|)$ + +$vSum_t = \frac{\sum_{i=t-buffer+1}^t volty_i}{buffer}$ + +$avgVolty_t = \text{mean}(vSum_{t-64:t})$ + +$rVolty_t = \text{clamp}(\frac{volty_t}{avgVolty_t}, 1, len1^{1/pow1})$ + +### Band Calculations: + +$pow2_t = rVolty_t^{pow1}$ + +$K_v = \beta^{\sqrt{pow2_t}}$ + + +$upperBand_t = price_t - K_v \cdot del1_t$ + + + +$\alpha_t = \beta^{pow2_t}$ + +$ma1_t = price_t + \alpha_t(ma1_{t-1} - price_t)$ + +$det0_t = price_t + \beta(det0_{t-1} - price_t + ma1_t) - ma1_t$ + +$ma2_t = ma1_t + phase \cdot det0_t$ + +$det1_t = (ma2_t - jma_{t-1})(1-\alpha_t)^2 + \alpha_t^2 \cdot det1_{t-1}$ + +$jma_t = jma_{t-1} + det1_t$ \ No newline at end of file diff --git a/docs/indicators/indicators.md b/docs/indicators/indicators.md index cb11589d..d0d66932 100644 --- a/docs/indicators/indicators.md +++ b/docs/indicators/indicators.md @@ -4,17 +4,17 @@ ✔️= Validation tests passed
❌= Issue -|**MOMENTUM INDICATORS**|**QuanTALib**|Skender.Stock|TALib.NETCore| +|**MOMENTUM INDICATORS**|**Class Name**|Skender.Stock|TALib.NETCore| |--|:--:|:--:|:--:| -|*DMI - Directional Movement Index|`?`|GetDmi|| -|*DMX - Jurik Directional Movement Index|`?`||| -|*MOM - Momentum|`?`||| -|*VEL - Jurik Signal Velocity|`?`||| +|DMI - Directional Movement Index|`?`|GetDmi|| +|DMX - Jurik Directional Movement Index|`?`||| +|MOM - Momentum|`?`||| +|VEL - Jurik Signal Velocity|`?`||| |ADX - Average Directional Movement Index|`?`|GetAdx|Adx| |ADXR - Average Directional Movement Index|`?`|Rating|Adxr| |APO - Absolute Price Oscillator|`?`|Apo|| |DPO - Detrended Price Oscillator|`?`|GetDpo|| -|MACD - Movign Average Convergence/Divergence|`?`||| +|MACD - Moving Average Convergence/Divergence|`?`||| |PO - Price Oscillator|`?`||| |PPO - Percentage Price Oscillator|`?`||| |PMO - Price Momentum Oscillator|`?`|GetPmo|| @@ -23,161 +23,161 @@ |TRIX - 1-day ROC of TEMA|`?`|GetTrix|| |VORTEX - Vortex Indicator|`?`|||
-|**VOLATILITY INDICATORS**|**QuanTALib**|Skender.Stock|TALib.NETCore| -|ADR - Average Daily Range|||| -|ANDREW - Andrew's Pitchfork|||| +|**VOLATILITY INDICATORS**|**Class Name**|Skender.Stock|TALib.NETCore| +|ADR - Average Daily Range|`?`||| +|ANDREW - Andrew's Pitchfork|`?`||| |ATR - Average True Range|`Atr`|GetAtr|Atr| -|ATRP - Average True Range Percent|||| -|ATRSTOP - ATR Trailing Stop ||GetAtrStop|| -|BBANDS - Bollinger Bands®||BollingerBands|| -|CHAND - Chandelier Exit||GetChandelier|| -|CVI - Chaikins Volatility|||| -|DON - Donchian Channels||GetDonchian|| -|FCB - Fractal Chaos Bands||GetFcb|| -|HV - Historical Volatility|||| -|ICH - Ichimoku Cloud||GetIchimoku|| -|KEL - Keltner Channels||GetKeltner|| -|NATR - Normalized Average True Range||GetAtr|| -|CHN - Price Channel Indicator|||| -|SAR - Parabolic Stop and Reverse||GetParabolicSar|| -|STARC - Starc Bands||GetStarcBands|| -|TR - True Range|||| -|UI - Ulcer Index||GetUlcerIndex|| -|VSTOP - Volatility Stop||GetVolatilityStop|| +|ATRP - Average True Range Percent|`?`||| +|ATRSTOP - ATR Trailing Stop|`?`|GetAtrStop|| +|BBANDS - Bollinger Bands®|`?`|BollingerBands|| +|CHAND - Chandelier Exit|`?`|GetChandelier|| +|CVI - Chaikins Volatility|`?`||| +|DON - Donchian Channels|`?`|GetDonchian|| +|FCB - Fractal Chaos Bands|`?`|GetFcb|| +|HV - Historical Volatility|`Hv`||| +|ICH - Ichimoku Cloud|`?`|GetIchimoku|| +|KEL - Keltner Channels|`?`|GetKeltner|| +|NATR - Normalized Average True Range|`?`|GetAtr|| +|CHN - Price Channel Indicator|`?`||| +|SAR - Parabolic Stop and Reverse|`?`|GetParabolicSar|| +|STARC - Starc Bands|`?`|GetStarcBands|| +|TR - True Range|`?`||| +|UI - Ulcer Index|`?`|GetUlcerIndex|| +|VSTOP - Volatility Stop|`?`|GetVolatilityStop||
-|**OSCILLATORS**|**QuanTALib**|Skender.Stock|TALib.NETCore| +|**OSCILLATORS**|**Class Name**|Skender.Stock|TALib.NETCore| |RSI - Relative Strength Index|`Rsi`|GetRsi|| |RSX - Jurik Trend Strength Index|`Rsx`||| -|AC - Acceleration Oscillator||||| -|AO - Awesome Oscillator||GetAwesome||| -|AROON - Aroon oscillator||GetAroon|Aroon|| -|BOP - Balance of Power||GetBop|Bop|| -|CCI - Commodity Channel Index||GetCci|Cci|| -|CFO - Chande Forcast Oscillator||||| -|CMO - Chande Momentum Oscillator||GetCmo|Cmo|| -|CHOP - Choppiness Index||GetChop||| -|COG - Ehler's Center of Gravity||||| -|COPPOCK - Coppock Curve||||| -|CRSI - Connor RSI||GetConnorsRsi||| -|CTI - Ehler's Correlation Trend Indicator||||| -|DOSC - Derivative Oscillator||||| -|EFI - Elder Ray's Force Index||GetElderRay||| -|FISHER - Fisher Transform||||| -|FOSC - Forecast Oscillator|||||| -|GATOR - Williams Alliator Oscillator||GetGator||| -|KDJ - KDJ Indicator (trend reversal)||||| -|KRI - Kairi Relative Index||||| -|RVGI - Relative Vigor Index||||| -|SMI - Stochastic Momentum Index||GetSmi||| -|SRSI - Stochastic RSI||GetStochRsi||| -|STC - Schaff Trend Cycle||GetStc||| -|STOCH - Stochastic Oscillator||`GetStoch||| -|TSI - True Strength Index||GetTsi||| -|UO - Ultimate Oscillator||GetUltimate||| -|WILLR - Larry Williams' %R||GetWilliamsR||| +|AC - Acceleration Oscillator|`?`||| +|AO - Awesome Oscillator|`?`|GetAwesome|| +|AROON - Aroon oscillator|`?`|GetAroon|Aroon| +|BOP - Balance of Power|`?`|GetBop|Bop| +|CCI - Commodity Channel Index|`?`|GetCci|Cci| +|CFO - Chande Forcast Oscillator|`?`||| +|CMO - Chande Momentum Oscillator|`Cmo`|GetCmo|Cmo| +|CHOP - Choppiness Index|`?`|GetChop|| +|COG - Ehler's Center of Gravity|`?`||| +|COPPOCK - Coppock Curve|`?`||| +|CRSI - Connor RSI|`?`|GetConnorsRsi|| +|CTI - Ehler's Correlation Trend Indicator|`?`||| +|DOSC - Derivative Oscillator|`?`||| +|EFI - Elder Ray's Force Index|`?`|GetElderRay|| +|FISHER - Fisher Transform|`?`||| +|FOSC - Forecast Oscillator|`?`||| +|GATOR - Williams Alliator Oscillator|`?`|GetGator|| +|KDJ - KDJ Indicator (trend reversal)|`?`||| +|KRI - Kairi Relative Index|`?`||| +|RVGI - Relative Vigor Index|`?`||| +|SMI - Stochastic Momentum Index|`?`|GetSmi|| +|SRSI - Stochastic RSI|`?`|GetStochRsi|| +|STC - Schaff Trend Cycle|`?`|GetStc|| +|STOCH - Stochastic Oscillator|`?`|GetStoch|| +|TSI - True Strength Index|`?`|GetTsi|| +|UO - Ultimate Oscillator|`?`|GetUltimate|| +|WILLR - Larry Williams' %R|`?`|GetWilliamsR||
-|**VOLUME INDICATORS**|**QuanTALib**|Skender.Stock|TALib.NETCore| -|ADL - Chaikin Accumulation Distribution Line||GetAdl|Ad|| -|ADOSC - Chaikin Accumulation Distribution Oscillator||GetChaikinOsc|AdOsc|| -|AOBV - Archer On-Balance Volume||||| -|CMF - Chaikin Money Flow||GetCmf||| -|EOM - Ease of Movement||||| -|KVO - Klinger Volume Oscillator||GetKvo|||| -|MFI - Money Flow Index||GetMfi||| -|NVI - Negative Volume Index||||| -|OBV - On-Balance Volume||GetObv||| -|PVI - Positive Volume Index||||| -|PVOL - Price-Volume||||| -|PVO - Percentage Volume Oscillator||GetPvo||| -|PVR - Price Volume Rank||||| -|PVT - Price Volume Trend||||| -|TVI - Trade Volume Index||||| -|VP - Volume Profile||||| -|VWAP - Volume Weighted Average Price||GetVwap||| -|VWMA - Volume Weighted Moving Average||GetVwma|||| +|**VOLUME INDICATORS**|**Class Name**|Skender.Stock|TALib.NETCore| +|ADL - Chaikin Accumulation Distribution Line|`?`|GetAdl|Ad| +|ADOSC - Chaikin Accumulation Distribution Oscillator|`?`|GetChaikinOsc|AdOsc| +|AOBV - Archer On-Balance Volume|`?`||| +|CMF - Chaikin Money Flow|`?`|GetCmf|| +|EOM - Ease of Movement|`?`||| +|KVO - Klinger Volume Oscillator|`?`|GetKvo|| +|MFI - Money Flow Index|`?`|GetMfi|| +|NVI - Negative Volume Index|`?`||| +|OBV - On-Balance Volume|`?`|GetObv|| +|PVI - Positive Volume Index|`?`||| +|PVOL - Price-Volume|`?`||| +|PVO - Percentage Volume Oscillator|`?`|GetPvo|| +|PVR - Price Volume Rank|`?`||| +|PVT - Price Volume Trend|`?`||| +|TVI - Trade Volume Index|`?`||| +|VP - Volume Profile|`?`||| +|VWAP - Volume Weighted Average Price|`?`|GetVwap|| +|VWMA - Volume Weighted Moving Average|`?`|GetVwma||
-|**NUMERICAL ANALYSIS**|**QuanTALib**|Skender.Stock|TALib.NETCore| -|BETA - Beta coefficient||||| -|CORR - Correlation Coefficient||||| -|CURVATURE - Rate of Change in Direction or Slope|`Curvature`|||| -|ENTROPY - Measure of Uncertainty or Disorder|`Entropy`|||| -|KURTOSIS - Measure of Tails/Peakedness|`Kurtosis`|||| -|HUBER - Huber Loss|`Huberloss`|||| -|HURST - Hurst Exponent||GetHurst||| -|MAX - Maximum with exponential decay|`Max`|||| -|MEDIAN - Middle value|`Median`|||| -|MIN - Minimum with exponential decay|`Min`|||| -|MODE - Most Frequent Value|`Mode`|||| -|PERCENTILE - Rank Order|`Percentile`|||| -|RSQUARED - Coefficient of Determination R-Squared||||| -|SKEW - Skewness, asymmetry of distribution|`Skew`|||| -|SLOPE - Rate of Change, Linear Regression|`Slope`|||| -|STDDEV - Standard Deviation, Measure of Spread|`Stddev`|||| -|THEIL - Theil's U Statistics||||| -|TSF - Time Series Forecast|||`✔️`|`✔️`| -|VARIANCE - Average of Squared Deviations|`Variance`|||| -|ZSCORE - Standardized Score|`Zscore`|||| +|**NUMERICAL ANALYSIS**|**Class Name**|Skender.Stock|TALib.NETCore| +|BETA - Beta coefficient|`?`||| +|CORR - Correlation Coefficient|`?`||| +|CURVATURE - Rate of Change in Direction or Slope|`Curvature`||| +|ENTROPY - Measure of Uncertainty or Disorder|`Entropy`||| +|KURTOSIS - Measure of Tails/Peakedness|`Kurtosis`||| +|HUBER - Huber Loss|`Huber`||| +|HURST - Hurst Exponent|`?`|GetHurst|| +|MAX - Maximum with exponential decay|`Max`||| +|MEDIAN - Middle value|`Median`||| +|MIN - Minimum with exponential decay|`Min`||| +|MODE - Most Frequent Value|`Mode`||| +|PERCENTILE - Rank Order|`Percentile`||| +|RSQUARED - Coefficient of Determination R-Squared|`?`||| +|SKEW - Skewness, asymmetry of distribution|`Skew`||| +|SLOPE - Rate of Change, Linear Regression|`Slope`||| +|STDDEV - Standard Deviation, Measure of Spread|`Stddev`||| +|THEIL - Theil's U Statistics|`?`||| +|TSF - Time Series Forecast|`?`|✔️|✔️| +|VARIANCE - Average of Squared Deviations|`Variance`||| +|ZSCORE - Standardized Score|`Zscore`|||
-|**ERRORS**|**QuanTALib**|Skender.Stock|TALib.NETCore| -|MAE - Mean Absolute Error|`Mae`|||| -|MAPD - Mean Absolute Percentage Deviation|`Mapd`|||| -|MAPE - Mean Absolute Percentage Error|`Mape`|||| -|MASE - Mean Absolute Scaled Error|`Mase`|||| -|MDA - Mean Directional Accuracy||||| -|ME - Mean Error|`Me`|||| -|MPE - Pean Percentage Error|`Mpe`|||| -|MSE - Mean Squared Error|`Mse`|||| -|MSLE - Mean Squared Logarithmic Error|`Msle`|||| -|RAE - Relative Absolute Error|`Rae`|||| -|RMSE - Root Mean Squared Error|`Rmse`|||| -|RSE - Relateive Squared Error|`Rse`|||| -|RMSLE - Root Mean Squared Logarithmic Error|`Rmsle`|||| -|SMAPE - Symmetric Mean Absolute Percentage Error|`Smape`|||| +|**ERRORS**|**Class Name**|Skender.Stock|TALib.NETCore| +|MAE - Mean Absolute Error|`Mae`||| +|MAPD - Mean Absolute Percentage Deviation|`Mapd`||| +|MAPE - Mean Absolute Percentage Error|`Mape`||| +|MASE - Mean Absolute Scaled Error|`Mase`||| +|MDA - Mean Directional Accuracy|`Mda`||| +|ME - Mean Error|`Me`||| +|MPE - Mean Percentage Error|`Mpe`||| +|MSE - Mean Squared Error|`Mse`||| +|MSLE - Mean Squared Logarithmic Error|`Msle`||| +|RAE - Relative Absolute Error|`Rae`||| +|RMSE - Root Mean Squared Error|`Rmse`||| +|RSE - Relative Squared Error|`Rse`||| +|RMSLE - Root Mean Squared Logarithmic Error|`Rmsle`||| +|SMAPE - Symmetric Mean Absolute Percentage Error|`Smape`|||
-|**AVERAGES & TRENDS**|**QuanTALib**|Skender.Stock|TALib.NETCore| -|AFIRMA - Autoregressive Finite Impulse Response Moving Average|`Afirma`|||| -|ALMA - Arnaud Legoux Moving Average|`Alma`|`✔️`|| -|DEMA - Double EMA Average|`Dema`|`✔️`|`✔️`| -|DSMA - Deviation Scaled Moving Average|`Dsma`|||| -|DWMA - Double WMA Average|`Dwma`|||| -|EMA - Exponential Moving Average|`Ema`|`⭐`|`⭐`| -|EPMA - Endpoint Moving Average|`Epma`|`✔️`||| -|FRAMA - Fractal Adaptive Moving Average|`Frama`|||| -|FWMA - Fibonacci Weighted Moving Average|`Fwma`|||| -|HILO - Gann High-Low Activator||||| -|HTIT - Hilbert Transform Instantaneous Trendline|`Htit`|`✔️`|`✔️`|| -|GMA - Gaussian-Weighted Moving Average|`Gma`|||| -|HMA - Hull Moving Average|`Hma`|`✔️`||`✔️`| -|HWMA - Holt-Winter Moving Average|`Hwma`|||| -|JMA - Jurik Moving Average|`Jma`|||| -|JORDAN - Jordan Moving Average||||| -|KAMA - Kaufman's Adaptive Moving Average|`Kama`|`✔️`|`✔️`|`✔️`| -|LTMA - Laguerre Transform Moving Average|`Ltma`|||| -|MAAF - Median-Average Adaptive Filter|`Maaf`|||| -|MAMA - MESA Adaptive Moving Average|`Mama`|`✔️`|`✔️`|| -|MGDI - McGinley Dynamic Indicator|`Mgdi`|`✔️`||| -|MLMA - Minimal Lag Moving Average||||| -|MMA - Modified Moving Average|`Mma`|||| -|PPMA - Pivot Point Moving Average||||| -|PWMA - Pascal's Weighted Moving Average|`Pwma`|||| -|QEMA - Quad Exponential Moving Average|`Qema`|||| -|RMA - WildeR's Moving Average|`Rma`|||| -|SINEMA - Sine Weighted Moving Average|`Sinema`|||| +|**AVERAGES & TRENDS**|**Class Name**|Skender.Stock|TALib.NETCore| +|AFIRMA - Autoregressive Finite Impulse Response Moving Average|`Afirma`||| +|ALMA - Arnaud Legoux Moving Average|`Alma`|✔️|| +|DEMA - Double EMA Average|`Dema`|✔️|✔️| +|DSMA - Deviation Scaled Moving Average|`Dsma`||| +|DWMA - Double WMA Average|`Dwma`||| +|EMA - Exponential Moving Average|`Ema`|⭐|⭐| +|EPMA - Endpoint Moving Average|`Epma`|✔️|| +|FRAMA - Fractal Adaptive Moving Average|`Frama`||| +|FWMA - Fibonacci Weighted Moving Average|`Fwma`||| +|HILO - Gann High-Low Activator|`?`||| +|HTIT - Hilbert Transform Instantaneous Trendline|`Htit`|✔️|✔️| +|GMA - Gaussian-Weighted Moving Average|`Gma`||| +|HMA - Hull Moving Average|`Hma`|✔️|✔️| +|HWMA - Holt-Winter Moving Average|`Hwma`||| +|JMA - Jurik Moving Average|`Jma`||| +|JORDAN - Jordan Moving Average|`?`||| +|KAMA - Kaufman's Adaptive Moving Average|`Kama`|✔️|✔️| +|LTMA - Laguerre Transform Moving Average|`Ltma`||| +|MAAF - Median-Average Adaptive Filter|`Maaf`||| +|MAMA - MESA Adaptive Moving Average|`Mama`|✔️|✔️| +|MGDI - McGinley Dynamic Indicator|`Mgdi`|✔️|| +|MLMA - Minimal Lag Moving Average|`?`||| +|MMA - Modified Moving Average|`Mma`||| +|PPMA - Pivot Point Moving Average|`?`||| +|PWMA - Pascal's Weighted Moving Average|`Pwma`||| +|QEMA - Quad Exponential Moving Average|`Qema`||| +|RMA - WildeR's Moving Average|`Rma`||| +|SINEMA - Sine Weighted Moving Average|`Sinema`||| |SMA - Simple Moving Average|`Sma`||| -|SMMA - Smoothed Moving Average|`Smma`|`✔️`|| -|SSF - Ehler's Super Smoother Filter|||| -|SUPERTREND - Supertrend||`✔️`|| -|T3 - Tillson T3 Moving Average|`T3`|`✔️`|`✔️`| -|TEMA - Triple EMA Average|`Tema`|`✔️`|`✔️`| -|TRIMA - Triangular Moving Average|`Trima`|`✔️`|| +|SMMA - Smoothed Moving Average|`Smma`|✔️|| +|SSF - Ehler's Super Smoother Filter|`?`||| +|SUPERTREND - Supertrend|`?`|✔️|| +|T3 - Tillson T3 Moving Average|`T3`|✔️|✔️| +|TEMA - Triple EMA Average|`Tema`|✔️|✔️| +|TRIMA - Triangular Moving Average|`Trima`|✔️|| |VIDYA - Variable Index Dynamic Average|`Vidya`||| -|WMA - Weighted Moving Average|`Wma`|`✔️`|| +|WMA - Weighted Moving Average|`Wma`|✔️|| |ZLEMA - Zero Lag EMA Average|`Zlema`|||
-|**BASIC TRANSFORMS**|**QuanTALib**|Skender.Stock|TALib.NETCore| -|OC2 - Midpoint price|️`.OC2`|CandlePart.OC2|MidPoint| -|HL2 - Median Price|️`.HL2`|CandlePart.HL2|MedPrice| -|HLC3 - Typical Price|️`.HLC3`|CandlePart.HLC3|TypPrice| -|OHL3 - Mean Price|`️.OHL3`|CandlePart.OHL3| -|OHLC4 - Average Price|`️.OHLC4`|CandlePart.OHLC4|AvgPrice| -|HLCC4 - Weighted Price|`️.HLCC4`||WclPrice| \ No newline at end of file +|**BASIC TRANSFORMS**|**Class Name**|Skender.Stock|TALib.NETCore| +|OC2 - Midpoint price|`.OC2`|CandlePart.OC2|MidPoint| +|HL2 - Median Price|`.HL2`|CandlePart.HL2|MedPrice| +|HLC3 - Typical Price|`.HLC3`|CandlePart.HLC3|TypPrice| +|OHL3 - Mean Price|`.OHL3`|CandlePart.OHL3|| +|OHLC4 - Average Price|`.OHLC4`|CandlePart.OHLC4|AvgPrice| +|HLCC4 - Weighted Price|`.HLCC4`||WclPrice| diff --git a/lib/averages/_list.md b/lib/averages/_list.md new file mode 100644 index 00000000..7ad3da91 --- /dev/null +++ b/lib/averages/_list.md @@ -0,0 +1,33 @@ +✔️ AFIRMA - Adaptive FIR Moving Average +✔️ ALMA - Arnaud Legoux Moving Average +✔️ DEMA - Double Exponential Moving Average +✔️ DSMA - Dynamic Simple Moving Average +✔️ DWMA - Dynamic Weighted Moving Average +✔️ EMA - Exponential Moving Average +✔️ EPMA - Endpoint Moving Average +✔️ FRAMA - Fractal Adaptive Moving Average +✔️ FWMA - Forward Weighted Moving Average +✔️ GMA - Gaussian Moving Average +✔️ HMA - Hull Moving Average +✔️ HTIT - Hilbert Transform Instantaneous Trendline +✔️ HWMA - Hann Weighted Moving Average +✔️ JMA - Jurik Moving Average +✔️ KAMA - Kaufman Adaptive Moving Average +✔️ LTMA - Linear Time Moving Average +✔️ MAAF - Moving Average Adaptive Filter +✔️ MAMA - MESA Adaptive Moving Average +✔️ MGDI - McGinley Dynamic Indicator +✔️ MMA - Modified Moving Average +✔️ PWMA - Parabolic Weighted Moving Average +✔️ QEMA - Quick Exponential Moving Average +✔️ REMA - Regularized Exponential Moving Average +✔️ RMA - Running Moving Average +✔️ SINEMA - Sine-weighted Moving Average +✔️ SMA - Simple Moving Average +✔️ SMMA - Smoothed Moving Average +✔️ T3 - Triple Exponential Moving Average (T3) +✔️ TEMA - Triple Exponential Moving Average +✔️ TRIMA - Triangular Moving Average +✔️ VIDYA - Variable Index Dynamic Average +✔️ WMA - Weighted Moving Average +✔️ ZLEMA - Zero-Lag Exponential Moving Average diff --git a/lib/errors/Huberloss.cs b/lib/errors/Huber.cs similarity index 92% rename from lib/errors/Huberloss.cs rename to lib/errors/Huber.cs index e2bdac55..c7510344 100644 --- a/lib/errors/Huberloss.cs +++ b/lib/errors/Huber.cs @@ -1,12 +1,12 @@ namespace QuanTAlib; -public class Huberloss : AbstractBase +public class Huber : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; private readonly double _delta; - public Huberloss(int period, double delta = 1.0) + public Huber(int period, double delta = 1.0) { if (period < 1) { @@ -24,7 +24,7 @@ public class Huberloss : AbstractBase Init(); } - public Huberloss(object source, int period, double delta = 1.0) : this(period, delta) + public Huber(object source, int period, double delta = 1.0) : this(period, delta) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); diff --git a/lib/errors/_list.md b/lib/errors/_list.md new file mode 100644 index 00000000..03ecde4b --- /dev/null +++ b/lib/errors/_list.md @@ -0,0 +1,16 @@ +✔️ HUBER - Huber Loss +✔️ MAE - Mean Absolute Error +✔️ MAPD - Mean Absolute Percentage Deviation +✔️ MAPE - Mean Absolute Percentage Error +✔️ MASE - Mean Absolute Scaled Error +✔️ MDA - Mean Directional Accuracy +✔️ ME - Mean Error +✔️ MPE - Mean Percentage Error +✔️ MSE - Mean Squared Error +✔️ MSLE - Mean Squared Logarithmic Error +✔️ RAE - Relative Absolute Error +✔️ RMSE - Root Mean Squared Error +✔️ RMSLE - Root Mean Squared Logarithmic Error +✔️ RSE - Relative Squared Error +✔️ RSQUARED - R-Squared (Coefficient of Determination) +✔️ SMAPE - Symmetric Mean Absolute Percentage Error diff --git a/lib/momentum/_list.md b/lib/momentum/_list.md new file mode 100644 index 00000000..873798b0 --- /dev/null +++ b/lib/momentum/_list.md @@ -0,0 +1,16 @@ +ADX - Average Directional Movement Index +ADXR - Average Directional Movement Index +APO - Absolute Price Oscillator +DMI - Directional Movement Index +DMX - Jurik Directional Movement Index +DPO - Detrended Price Oscillator +MACD - Moving Average Convergence/Divergence +MOM - Momentum +PMO - Price Momentum Oscillator +PO - Price Oscillator +PPO - Percentage Price Oscillator +PRS - Price Relative Strength +ROC - Rate of Change +TRIX - 1-day ROC of TEMA +VEL - Jurik Signal Velocity +VORTEX - Vortex Indicator diff --git a/lib/oscillators/Cmo.cs b/lib/oscillators/Cmo.cs index 0b8d92c5..db0c545c 100644 --- a/lib/oscillators/Cmo.cs +++ b/lib/oscillators/Cmo.cs @@ -20,6 +20,17 @@ public class Cmo : AbstractBase Name = $"CMO({period})"; } + /// + /// Initializes a new instance of the CMO class with a data source. + /// + /// The source object that publishes data. + /// The number of data points to consider. + public Cmo(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + protected override void ManageState(bool isNew) { if (isNew) @@ -54,7 +65,6 @@ public class Cmo : AbstractBase { _sumH.Add(0, Input.IsNew); _sumL.Add(-diff, Input.IsNew); - } // Calculate sums for the specified period only @@ -67,4 +77,3 @@ public class Cmo : AbstractBase 0.0; } } - diff --git a/lib/volatility/Rsi.cs b/lib/oscillators/Rsi.cs similarity index 76% rename from lib/volatility/Rsi.cs rename to lib/oscillators/Rsi.cs index 9252281d..358e4d03 100644 --- a/lib/volatility/Rsi.cs +++ b/lib/oscillators/Rsi.cs @@ -22,6 +22,17 @@ public class Rsi : AbstractBase Name = $"RSI({period})"; } + /// + /// Initializes a new instance of the RSI class with a data source. + /// + /// The source object that publishes data. + /// The number of data points to consider. + public Rsi(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + protected override void ManageState(bool isNew) { if (isNew) @@ -53,10 +64,7 @@ public class Rsi : AbstractBase _avgLoss.Calc(loss, IsNew: Input.IsNew); double rsi = (_avgLoss.Value > 0) ? 100 - (100 / (1 + (_avgGain.Value / _avgLoss.Value))) : 100; - return rsi; - - } } diff --git a/lib/volatility/Rsx.cs b/lib/oscillators/Rsx.cs similarity index 70% rename from lib/volatility/Rsx.cs rename to lib/oscillators/Rsx.cs index 6d44c26d..b2d365c6 100644 --- a/lib/volatility/Rsx.cs +++ b/lib/oscillators/Rsx.cs @@ -24,6 +24,19 @@ public class Rsx : AbstractBase Name = $"RSX({period})"; } + /// + /// Initializes a new instance of the RSX class with a data source. + /// + /// The source object that publishes data. + /// The number of data points to consider. + /// The phase parameter. + /// The factor parameter. + public Rsx(object source, int period, int phase = 0, double factor = 0.55) : this(period, phase, factor) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + protected override void ManageState(bool isNew) { if (isNew) @@ -58,7 +71,5 @@ public class Rsx : AbstractBase double rsx = _rsx.Calc(rsi, Input.IsNew); return rsx; - - } } diff --git a/lib/oscillators/_list.md b/lib/oscillators/_list.md new file mode 100644 index 00000000..ecda4263 --- /dev/null +++ b/lib/oscillators/_list.md @@ -0,0 +1,29 @@ +AC - Acceleration Oscillator +AO - Awesome Oscillator +AROON - Aroon oscillator +BOP - Balance of Power +CCI - Commodity Channel Index +CFO - Chande Forcast Oscillator +✔️ CMO - Chande Momentum Oscillator +CHOP - Choppiness Index +COG - Ehler's Center of Gravity +COPPOCK - Coppock Curve +CRSI - Connor RSI +CTI - Ehler's Correlation Trend Indicator +DOSC - Derivative Oscillator +EFI - Elder Ray's Force Index +FISHER - Fisher Transform +FOSC - Forecast Oscillator +GATOR - Williams Alliator Oscillator +KDJ - KDJ Indicator (trend reversal) +KRI - Kairi Relative Index +✔️ RSI - Relative Strength Index +✔️ RSX - Jurik Trend Strength Index +RVGI - Relative Vigor Index +SMI - Stochastic Momentum Index +SRSI - Stochastic RSI +STC - Schaff Trend Cycle +STOCH - Stochastic Oscillator +TSI - True Strength Index +UO - Ultimate Oscillator +WILLR - Larry Williams' %R diff --git a/lib/statistics/_list.md b/lib/statistics/_list.md new file mode 100644 index 00000000..b7f77ddc --- /dev/null +++ b/lib/statistics/_list.md @@ -0,0 +1,20 @@ +BETA - Beta coefficient +CORR - Correlation Coefficient +✔️ CURVATURE - Rate of Change in Direction or Slope +✔️ ENTROPY - Measure of Uncertainty or Disorder +HUBER - Huber Loss +HURST - Hurst Exponent +✔️ KURTOSIS - Measure of Tails/Peakedness +✔️ MAX - Maximum with exponential decay +✔️ MEDIAN - Middle value +✔️ MIN - Minimum with exponential decay +✔️ MODE - Most Frequent Value +✔️ PERCENTILE - Rank Order +RSQUARED - Coefficient of Determination R-Squared +✔️ SKEW - Skewness, asymmetry of distribution +✔️ SLOPE - Rate of Change, Linear Regression +✔️ STDDEV - Standard Deviation, Measure of Spread +THEIL - Theil's U Statistics +TSF - Time Series Forecast +✔️ VARIANCE - Average of Squared Deviations +✔️ ZSCORE - Standardized Score diff --git a/lib/volatility/Historical.cs b/lib/volatility/Hv.cs similarity index 95% rename from lib/volatility/Historical.cs rename to lib/volatility/Hv.cs index 9417b14d..ef74e37e 100644 --- a/lib/volatility/Historical.cs +++ b/lib/volatility/Hv.cs @@ -9,7 +9,7 @@ namespace QuanTAlib; /// both annualized and non-annualized volatility measures. The calculation uses a sample /// standard deviation formula and assumes 252 trading days in a year for annualization. /// -public class Historical : AbstractBase +public class Hv : AbstractBase { private readonly int Period; private readonly bool IsAnnualized; @@ -25,7 +25,7 @@ public class Historical : AbstractBase /// /// Thrown when period is less than 2. /// - public Historical(int period, bool isAnnualized = true) + public Hv(int period, bool isAnnualized = true) { if (period < 2) { @@ -46,7 +46,7 @@ public class Historical : AbstractBase /// The source object to subscribe to for value updates. /// The period over which to calculate historical volatility. /// Whether to annualize the volatility (default is true). - public Historical(object source, int period, bool isAnnualized = true) : this(period, isAnnualized) + public Hv(object source, int period, bool isAnnualized = true) : this(period, isAnnualized) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); diff --git a/lib/volatility/Realized.cs b/lib/volatility/Rv.cs similarity index 85% rename from lib/volatility/Realized.cs rename to lib/volatility/Rv.cs index 5798919b..4aec3783 100644 --- a/lib/volatility/Realized.cs +++ b/lib/volatility/Rv.cs @@ -9,7 +9,7 @@ namespace QuanTAlib; /// both annualized and non-annualized volatility measures. The calculation uses a rolling /// sum of squared returns for efficiency and assumes 252 trading days in a year for annualization. /// -public class Realized : AbstractBase +public class Rv : AbstractBase { private readonly int Period; private readonly bool IsAnnualized; @@ -25,7 +25,7 @@ public class Realized : AbstractBase /// /// Thrown when period is less than 2. /// - public Realized(int period, bool isAnnualized = true) + public Rv(int period, bool isAnnualized = true) { if (period < 2) { @@ -39,6 +39,18 @@ public class Realized : AbstractBase Init(); } + /// + /// Initializes a new instance of the Realized class with a data source. + /// + /// The source object that publishes data. + /// The period over which to calculate realized volatility. + /// Whether to annualize the volatility (default is true). + public Rv(object source, int period, bool isAnnualized = true) : this(period, isAnnualized) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + /// /// Initializes the Realized instance by clearing buffers and resetting calculation variables. /// @@ -113,4 +125,4 @@ public class Realized : AbstractBase IsHot = _index >= WarmupPeriod; return volatility; } -} \ No newline at end of file +} diff --git a/lib/volatility/_list.md b/lib/volatility/_list.md new file mode 100644 index 00000000..5f62b5cf --- /dev/null +++ b/lib/volatility/_list.md @@ -0,0 +1,34 @@ +ADR - Average Daily Range +AP - Andrew's Pitchfork +✔️ ATR - Average True Range +ATRP - Average True Range Percent +ATRS - ATR Trailing Stop +BB - Bollinger Bands® +CCV - Close-to-Close Volatility +CE - Chandelier Exit +CV - Conditional Volatility (ARCH/GARCH) +CVI - Chaikin's Volatility +DC - Donchian Channels +FCB - Fractal Chaos Bands +GKV - Garman-Klass Volatility +HLV - High-Low Volatility +✔️ HV - Historical Volatility +ICH - Ichimoku Cloud +✔️ JVOLTY - Jurik Volatility +KC - Keltner Channels +NATR - Normalized Average True Range +PCH - Price Channel Indicator +PSAR - Parabolic Stop and Reverse +PV - Parkinson Volatility +RSV - Rogers-Satchell Volatility +✔️ RV - Realized Volatility +RVI - Relative Volatility Index +STARC - Starc Bands +SV - Stochastic Volatility +TR - True Range +UI - Ulcer Index +VC - Volatility Cone +VOV - Volatility of Volatility +VR - Volatility Ratio +VS - Volatility Stop +YZV - Yang-Zhang Volatility \ No newline at end of file diff --git a/lib/volume/_list.md b/lib/volume/_list.md new file mode 100644 index 00000000..0b99daa1 --- /dev/null +++ b/lib/volume/_list.md @@ -0,0 +1,18 @@ +ADL - Chaikin Accumulation Distribution Line +ADOSC - Chaikin Accumulation Distribution Oscillator +AOBV - Archer On-Balance Volume +CMF - Chaikin Money Flow +EOM - Ease of Movement +KVO - Klinger Volume Oscillator +MFI - Money Flow Index +NVI - Negative Volume Index +OBV - On-Balance Volume +PVI - Positive Volume Index +PVOL - Price-Volume +PVO - Percentage Volume Oscillator +PVR - Price Volume Rank +PVT - Price Volume Trend +TVI - Trade Volume Index +VP - Volume Profile +VWAP - Volume Weighted Average Price +VWMA - Volume Weighted Moving Average diff --git a/notebooks/charting.dib b/notebooks/charting.dib index 93f47139..6aaaed19 100644 --- a/notebooks/charting.dib +++ b/notebooks/charting.dib @@ -4,7 +4,7 @@ #!csharp -#r "..\lib\obj\Debug\QuanTAlib.dll" +#r "../lib/obj/Debug/QuanTAlib.dll" using QuanTAlib; QuanTAlib.Formatters.Initialize(); @@ -40,14 +40,14 @@ Formatter.Register(typeof(ScottPlot.Plot), (p, w) => TSeries ma1 = Spike; TSeries out1 = new(); -Ema calc1 = new(10); +Jma calc1 = new(period: 7, phase: 0, factor: 0.30, buffer: 2); foreach (var value in ma1) { out1.Add(calc1.Calc(value)); } double[] gma1 = ma1.v.ToArray()[52..]; double[] gsig1 = out1.v.ToArray()[52..]; TSeries ma2 = Impulse; TSeries out2 = new(); -Ema calc2 = new(10); +Jma calc2 = new(period: 7, phase: 0, factor: 0.20, buffer: 2); foreach (var value in ma2) { out2.Add(calc2.Calc(value)); } double[] gma2 = ma2.v.ToArray()[52..]; double[] gsig2 = out2.v.ToArray()[52..]; @@ -57,12 +57,12 @@ double[] gsig2 = out2.v.ToArray()[52..]; Plot plt1 = new(); var p1a = plt1.Add.Signal(gma1); p1a.Color = ScottPlot.Colors.Red; p1a.LineWidth = 2; var p1b = plt1.Add.Signal(gsig1); p1b.Color = ScottPlot.Colors.Blue; p1b.LineWidth = 3; -plt1.Title("Spike - EMA(10)"); +plt1.Title("Spike - JMA(10)"); Plot plt2 = new(); var p2a = plt2.Add.Signal(gma2); p2a.Color = ScottPlot.Colors.Red; p2a.LineWidth = 2; var p2b = plt2.Add.Signal(gsig2); p2b.Color = ScottPlot.Colors.Blue; p2b.LineWidth = 3; -plt2.Title("Impulse - EMA(10)"); +plt2.Title("Impulse - JMA(10)"); plt1.Display(); plt2.Display(); diff --git a/notebooks/jma.dib b/notebooks/jma.dib index b6d42f87..72b779d7 100644 --- a/notebooks/jma.dib +++ b/notebooks/jma.dib @@ -41,7 +41,7 @@ plt.Display(); #!csharp -#r "..\lib\obj\Debug\QuanTAlib.dll" +#r "../lib/obj/Debug/QuanTAlib.dll" using QuanTAlib; QuanTAlib.Formatters.Initialize(); @@ -82,163 +82,11 @@ TSeries MarketJMA = new() { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #!csharp -public class Jmaxx : AbstractBase -{ - private readonly double _period; - private readonly double _phase; - private readonly CircularBuffer _vsumBuff; - private readonly CircularBuffer _avoltyBuff; - - private double _len1; - private double _pow1; - private readonly double _beta; - private double _upperBand, _lowerBand, _p_upperBand, _p_lowerBand; - private double _prevMa1, _prevDet0, _prevDet1, _prevJma, _p_prevMa1, _p_prevDet0, _p_prevDet1, _p_prevJma; - private double _vSum, _p_vSum; - - - public double UpperBand { get; set; } - public double LowerBand { get; set; } - public double Volty { get; set; } - - /// - /// Initializes a new instance of the Jma class with the specified parameters. - /// - /// The period over which to calculate the Jvolty. - /// The phase parameter for the JMA-style calculation. - /// - /// Thrown when period is less than 1. - /// - public Jmaxx(int period, int phase = 0) - { - if (period < 1) - { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); - } - _period = period; - - _phase = Math.Clamp((phase * 0.01) + 1.5, 0.5, 2.5); - - _vsumBuff = new CircularBuffer(10); - _avoltyBuff = new CircularBuffer(65); - _beta = 0.45 * (_period - 1) / (0.45 * (_period - 1) + 2); - - WarmupPeriod = (int)_period * 2; - Name = $"JMA({period})"; - } - - /// - /// Initializes the Jma instance by setting up the initial state. - /// - public override void Init() - { - base.Init(); - _upperBand = _lowerBand = 0.0; - _p_upperBand = _p_lowerBand = 0.0; - _len1 = Math.Max((Math.Log(Math.Sqrt(_period - 1)) / Math.Log(2.0)) + 2.0, 0); - _pow1 = Math.Max(_len1 - 2.0, 0.5); - _avoltyBuff.Clear(); - _vsumBuff.Clear(); - } - - /// - /// Manages the state of the Jma instance based on whether a new value is being processed. - /// - /// Indicates whether the current input is a new value. - protected override void ManageState(bool isNew) - { - if (isNew) - { - _index++; - _p_upperBand = _upperBand; - _p_lowerBand = _lowerBand; - _p_vSum = _vSum; - _p_prevMa1 = _prevMa1; - _p_prevDet0 = _prevDet0; - _p_prevDet1 = _prevDet1; - _p_prevJma = _prevJma; - } - else - { - _upperBand = _p_upperBand; - _lowerBand = _p_lowerBand; - _vSum = _p_vSum; - _prevMa1 = _p_prevMa1; - _prevDet0 = _p_prevDet0; - _prevDet1 = _p_prevDet1; - _prevJma = _p_prevJma; - } - } - - /// - /// Performs the Jma calculation for the current value. - /// - /// - /// The calculated Jma value for the current input. - /// - protected override double Calculation() - { - ManageState(Input.IsNew); - - double price = Input.Value; - if (_index == 1) - { - _upperBand = _lowerBand = price; - } - - double del1 = price - _upperBand; - double del2 = price - _lowerBand; - double volty = Math.Max(Math.Abs(del1), Math.Abs(del2)); - - _vsumBuff.Add(volty, Input.IsNew); - _vSum += (_vsumBuff[^1] - _vsumBuff[0]) / 10; - _avoltyBuff.Add(_vSum, Input.IsNew); - double avgvolty = _avoltyBuff.Average(); - - double rvolty = (avgvolty > 0) ? volty / avgvolty : 1; - rvolty = Math.Min(Math.Max(rvolty, 1.0), Math.Pow(_len1, 1.0 / _pow1)); - - double pow2 = Math.Pow(rvolty, _pow1); - double Kv = Math.Pow(_beta, Math.Sqrt(pow2)); - - _upperBand = (del1 >= 0) ? price : price - (Kv * del1); - _lowerBand = (del2 <= 0) ? price : price - (Kv * del2); - - - - - - - - double alpha = Math.Pow(_beta, pow2); - double ma1 = (1 - alpha) * Input.Value + alpha * _prevMa1; - _prevMa1 = ma1; - - double det0 = (price - ma1) * (1 - _beta) + _beta * _prevDet0; - _prevDet0 = det0; - double ma2 = ma1 + _phase * det0; - - double det1 = ((ma2 - _prevJma) * (1 - alpha) * (1 - alpha) ) + (alpha * alpha * _prevDet1); - _prevDet1 = det1; - double jma = _prevJma + det1; - _prevJma = jma; - - UpperBand = _upperBand; - LowerBand = _lowerBand; - Volty = volty; - - IsHot = _index >= WarmupPeriod; - return jma; - } -} - -#!csharp - -TSeries ma = Complex; -TSeries re = ComplexJMA; +TSeries ma = Spike; +TSeries re = SpikeJMA; TSeries out1 = new(); -Jmaxx calc = new(10); +Jma calc = new(period: 10, phase: 0, factor: 0.45); foreach (var value in ma) { out1.Add(calc.Calc(value)); } diff --git a/quantower/Averages/ZlemaIndicator.cs b/quantower/Averages/ZlemaIndicator.cs index a38b9c07..a6ffd28e 100644 --- a/quantower/Averages/ZlemaIndicator.cs +++ b/quantower/Averages/ZlemaIndicator.cs @@ -26,7 +26,7 @@ public class ZlemaIndicator : Indicator, IWatchlistIndicator public bool ShowColdValues { get; set; } = true; private Zlema? ma; - private Huberloss? err; + private Huber? err; protected LineSeries? Series; protected string? SourceName; public int MinHistoryDepths => Periods; diff --git a/quantower/Averages/_Averages.csproj b/quantower/Averages/_Averages.csproj index cadffce4..8d946a96 100644 --- a/quantower/Averages/_Averages.csproj +++ b/quantower/Averages/_Averages.csproj @@ -26,8 +26,10 @@ - - + + diff --git a/quantower/Statistics/_Statistics.csproj b/quantower/Statistics/_Statistics.csproj index 3f3ccd37..4904fae6 100644 --- a/quantower/Statistics/_Statistics.csproj +++ b/quantower/Statistics/_Statistics.csproj @@ -25,7 +25,8 @@ - + diff --git a/quantower/Volatility/HistoricalIndicator.cs b/quantower/Volatility/HistoricalIndicator.cs index 06ed5ec8..949b2dde 100644 --- a/quantower/Volatility/HistoricalIndicator.cs +++ b/quantower/Volatility/HistoricalIndicator.cs @@ -11,7 +11,7 @@ public class HistoricalIndicator : Indicator, IWatchlistIndicator [InputParameter("Annualized", sortIndex: 2)] public bool IsAnnualized { get; set; } = true; - private Historical? historical; + private Hv? historical; protected LineSeries? HvSeries; public int MinHistoryDepths => Periods; int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths; @@ -28,7 +28,7 @@ public class HistoricalIndicator : Indicator, IWatchlistIndicator protected override void OnInit() { - historical = new Historical(Periods, IsAnnualized); + historical = new(Periods, IsAnnualized); base.OnInit(); } diff --git a/quantower/Volatility/RealizedIndicator.cs b/quantower/Volatility/RealizedIndicator.cs index 126658c7..6625a71f 100644 --- a/quantower/Volatility/RealizedIndicator.cs +++ b/quantower/Volatility/RealizedIndicator.cs @@ -11,7 +11,7 @@ public class RealizedIndicator : Indicator, IWatchlistIndicator [InputParameter("Annualized", sortIndex: 2)] public bool IsAnnualized { get; set; } = true; - private Realized? realized; + private Rv? realized; protected LineSeries? RvSeries; public int MinHistoryDepths => Periods; int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths; @@ -28,7 +28,7 @@ public class RealizedIndicator : Indicator, IWatchlistIndicator protected override void OnInit() { - realized = new Realized(Periods, IsAnnualized); + realized = new(Periods, IsAnnualized); base.OnInit(); } diff --git a/quantower/Volatility/_Volatility.csproj b/quantower/Volatility/_Volatility.csproj index eabfa6b9..0a8f58d8 100644 --- a/quantower/Volatility/_Volatility.csproj +++ b/quantower/Volatility/_Volatility.csproj @@ -26,7 +26,8 @@ - + From b2fcdda7855fa8de82b3ca154e885932e993cbce Mon Sep 17 00:00:00 2001 From: Miha Date: Sun, 27 Oct 2024 09:38:53 -0700 Subject: [PATCH 04/17] xml doc rewrite --- .github/workflows/Publish.yml | 9 ++- .sonarlint/QuanTAlib.json | 2 +- .vscode/launch.json | 33 ++++++++++ .vscode/settings.json | 4 +- .vscode/tasks.json | 62 ++++++++++++++++++ Tests/Tests.csproj | 4 ++ lib/averages/Afirma.cs | 21 ++++++- lib/averages/Convolution.cs | 17 ++++- lib/averages/Epma.cs | 34 +++++++++- lib/averages/Frama.cs | 29 ++++++++- lib/averages/Fwma.cs | 35 ++++++++++- lib/averages/Gma.cs | 36 ++++++++++- lib/averages/Hma.cs | 37 ++++++++++- lib/averages/Htit.cs | 40 ++++++++++-- lib/averages/Hwma.cs | 42 ++++++++++++- lib/averages/Jma.cs | 68 +++++++++++--------- lib/averages/Kama.cs | 35 ++++++++++- lib/averages/Ltma.cs | 33 +++++++++- lib/averages/Maaf.cs | 37 +++++++++-- lib/averages/Mama.cs | 35 +++++++++++ lib/averages/Mgdi.cs | 34 +++++++++- lib/averages/Mma.cs | 34 ++++++++++ lib/averages/Pwma.cs | 36 +++++++++++ lib/averages/Qema.cs | 40 +++++++++++- lib/averages/Rema.cs | 42 ++++++++++++- lib/averages/Sinema.cs | 37 ++++++++++- lib/averages/Sma.cs | 36 +++++++++-- lib/averages/Smma.cs | 32 +++++++++- lib/averages/T3.cs | 38 ++++++++++- lib/averages/Tema.cs | 34 +++++++++- lib/averages/Trima.cs | 37 ++++++++++- lib/averages/Vidya.cs | 41 +++++++++++- lib/averages/Wma.cs | 38 ++++++++++- lib/averages/Zlema.cs | 32 ++++++++++ lib/errors/Huber.cs | 40 +++++++++++- lib/errors/Mae.cs | 34 +++++++++- lib/errors/Mapd.cs | 36 ++++++++++- lib/errors/Mape.cs | 35 +++++++++++ lib/errors/Mase.cs | 63 ++++++++++++------- lib/errors/Mda.cs | 35 +++++++++++ lib/errors/Me.cs | 35 +++++++++++ lib/errors/Mpe.cs | 36 +++++++++++ lib/errors/Mse.cs | 65 +++++++++---------- lib/errors/Msle.cs | 36 +++++++++++ lib/errors/Rae.cs | 35 +++++++++++ lib/errors/Rmse.cs | 36 +++++++++++ lib/errors/Rmsle.cs | 37 +++++++++++ lib/errors/Rse.cs | 36 +++++++++++ lib/errors/Rsquared.cs | 36 +++++++++++ lib/errors/Smape.cs | 35 +++++++++++ lib/oscillators/Cmo.cs | 49 ++++++++++++--- lib/oscillators/Rsi.cs | 46 +++++++++++--- lib/oscillators/Rsx.cs | 53 +++++++++++++--- lib/statistics/Curvature.cs | 71 ++++++++++----------- lib/statistics/Entropy.cs | 93 ++++++++++++++------------- lib/statistics/Kurtosis.cs | 99 +++++++++++++---------------- lib/statistics/Max.cs | 111 +++++++++++++------------------- lib/statistics/Median.cs | 85 +++++++++++++------------ lib/statistics/Min.cs | 111 +++++++++++++------------------- lib/statistics/Mode.cs | 100 ++++++++++++++--------------- lib/statistics/Percentile.cs | 101 ++++++++++++++--------------- lib/statistics/Skew.cs | 109 +++++++++++++++----------------- lib/statistics/Slope.cs | 114 +++++++++++++++------------------ lib/statistics/Stddev.cs | 114 +++++++++++++++------------------ lib/statistics/Variance.cs | 115 +++++++++++++++------------------- lib/statistics/Zscore.cs | 113 +++++++++++++++------------------ lib/volatility/Atr.cs | 83 +++++++++++++----------- lib/volatility/Hv.cs | 99 ++++++++++++++++------------- lib/volatility/Jvolty.cs | 97 ++++++++++++++++------------ lib/volatility/Rv.cs | 99 +++++++++++++++-------------- lib/volatility/Rvi.cs | 93 ++++++++++++++------------- 71 files changed, 2607 insertions(+), 1102 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json diff --git a/.github/workflows/Publish.yml b/.github/workflows/Publish.yml index 44d7bdaf..9fb5e4f9 100644 --- a/.github/workflows/Publish.yml +++ b/.github/workflows/Publish.yml @@ -1,7 +1,3 @@ -# This workflow integrates SonarCloud analysis, coverage reporting, -# CodeQL analysis, SecurityCodeScan, and Codacy Security Scan -# for code scanning and vulnerability detection - and if they all pass, publish - name: Publish Workflow on: @@ -82,7 +78,10 @@ jobs: -Dsonar.sources=. -Dsonar.cs.opencover.reportsPaths=**/*cover*.xml -Dsonar.cs.dotcover.reportsPaths=**/dotcover.xml - -Dsonar.coverage.exclusions=**Tests.cs + -Dsonar.coverage.exclusions=**Tests.cs,**/*.md,**/*.html,**/*.css,**/docs/**/*,**/archive/**/*,**/notebooks/**/* + -Dsonar.exclusions=**/TestResults/**/*,**/bin/**/*,**/obj/**/*,**/*.html,**/coverage/**/*,**/CoverageReport/**/*,**/*.md,**/*.css,**/docs/**/*,**/archive/**/*,**/notebooks/**/* + -Dsonar.cpd.exclusions=**Tests.cs + -Dsonar.test.exclusions=**Tests.cs CodeQL: runs-on: ubuntu-latest diff --git a/.sonarlint/QuanTAlib.json b/.sonarlint/QuanTAlib.json index f75a1bc1..c44cece3 100644 --- a/.sonarlint/QuanTAlib.json +++ b/.sonarlint/QuanTAlib.json @@ -1,4 +1,4 @@ { - "sonarCloudOrganization": "mihakralj-quantalib", + "sonarCloudOrganization": "mihakralj", "projectKey": "mihakralj_QuanTAlib" } \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..dcc54733 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,33 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Tests", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "dotnet", + "args": [ + "test", + "${workspaceFolder}/QuanTAlib.sln", + "--no-build" + ], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "console": "internalConsole", + "logging": { + "moduleLoad": false + } + }, + { + "name": "Debug Library", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}", + "justMyCode": true, + "logging": { + "moduleLoad": false + } + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index dbbc4e17..335e6e7c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,9 +7,9 @@ } ], "sarif-viewer.connectToGithubCodeScanning": "on", - "dotnet.dotnetPath": "C:/Program Files/dotnet", + "dotnet.dotnetPath": "/opt/homebrew/bin/", "omnisharp.useModernNet": true, - "omnisharp.sdkPath": "C:/Program Files/dotnet/sdk", + "omnisharp.sdkPath": "/opt/homebrew/bin/dotnet", "sonarlint.connectedMode.project": { "connectionId": "mihakralj", "projectKey": "mihakralj_QuanTAlib" diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..33d40edd --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,62 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/QuanTAlib.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile", + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "test", + "command": "dotnet", + "type": "process", + "args": [ + "test", + "${workspaceFolder}/QuanTAlib.sln", + "--no-build", + "--verbosity:normal" + ], + "problemMatcher": "$msCompile", + "group": { + "kind": "test", + "isDefault": true + }, + "dependsOn": ["build"] + }, + { + "label": "test with coverage", + "command": "dotnet", + "type": "process", + "args": [ + "test", + "${workspaceFolder}/QuanTAlib.sln", + "/p:CollectCoverage=true", + "/p:CoverletOutputFormat=lcov", + "/p:CoverletOutput=./lcov.info", + "--no-build" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "clean", + "command": "dotnet", + "type": "process", + "args": [ + "clean", + "${workspaceFolder}/QuanTAlib.sln" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 18f54f05..11c1e048 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -4,6 +4,10 @@ QuanTAlib.Tests
+ + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/lib/averages/Afirma.cs b/lib/averages/Afirma.cs index 3832ad55..e80f2caf 100644 --- a/lib/averages/Afirma.cs +++ b/lib/averages/Afirma.cs @@ -1,8 +1,18 @@ +using System; namespace QuanTAlib; +/// +/// AFIRMA: Adaptive FIR Moving Average +/// A finite impulse response (FIR) filter that combines windowing functions with sinc-based filtering. +/// Provides superior noise reduction while maintaining signal fidelity through adaptive filtering. +/// +/// +/// Implementation: +/// Original implementation based on FIR filter design principles +/// + public class Afirma : AbstractBase { - public enum WindowType { Rectangular, @@ -22,6 +32,10 @@ public class Afirma : AbstractBase private readonly int _n; private readonly double _sx2, _sx3, _sx4, _sx5, _sx6, _den; + /// The number of periods for the sinc filter calculation. + /// The number of filter taps (filter length). Must be odd number. + /// The type of window function to apply (Rectangular, Hanning1, Hanning2, Blackman, or BlackmanHarris). + /// Thrown when periods or taps is less than 1. public Afirma(int periods, int taps, WindowType window) { if (periods < 1) @@ -54,6 +68,10 @@ public class Afirma : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of periods for the sinc filter calculation. + /// The number of filter taps (filter length). Must be odd number. + /// The type of window function to apply (Rectangular, Hanning1, Hanning2, Blackman, or BlackmanHarris). public Afirma(object source, int periods, int taps, WindowType window) : this(periods, taps, window) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -147,5 +165,4 @@ public class Afirma : AbstractBase } return wsum; } - } diff --git a/lib/averages/Convolution.cs b/lib/averages/Convolution.cs index 60226d61..4a7a75fc 100644 --- a/lib/averages/Convolution.cs +++ b/lib/averages/Convolution.cs @@ -1,5 +1,16 @@ +using System; namespace QuanTAlib; +/// +/// Convolution: A fundamental signal processing operation that combines two signals to form a third signal +/// Applies a custom kernel (weight array) to the input data through convolution, allowing for flexible +/// filtering operations. The kernel is automatically normalized to ensure consistent output scaling. +/// +/// +/// Implementation: +/// Based on standard discrete convolution principles from signal processing +/// + public class Convolution : AbstractBase { private readonly double[] _kernel; @@ -7,6 +18,8 @@ public class Convolution : AbstractBase private readonly CircularBuffer _buffer; private readonly double[] _normalizedKernel; + /// Array of weights defining the convolution operation. The length of this array determines the filter's window size. + /// Thrown when kernel is null or empty. public Convolution(double[] kernel) { if (kernel == null || kernel.Length == 0) @@ -20,6 +33,8 @@ public class Convolution : AbstractBase Init(); } + /// The data source object that publishes updates. + /// Array of weights defining the convolution operation. public Convolution(object source, double[] kernel) : this(kernel) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -100,4 +115,4 @@ public class Convolution : AbstractBase return sum; } -} \ No newline at end of file +} diff --git a/lib/averages/Epma.cs b/lib/averages/Epma.cs index 4d55e203..f9985e9b 100644 --- a/lib/averages/Epma.cs +++ b/lib/averages/Epma.cs @@ -1,10 +1,35 @@ +using System; namespace QuanTAlib; +/// +/// EPMA: Endpoint Moving Average +/// A moving average that uses a specialized convolution kernel to emphasize recent price movements +/// while maintaining a connection to historical data. The weights decrease linearly with a focus +/// on endpoints. +/// +/// +/// The EPMA uses a unique weighting scheme where: +/// - The most recent price gets the highest weight: (2 * period - 1) +/// - Each previous price gets a weight reduced by 3: (2 * period - 1) - 3i +/// - Weights are normalized to sum to 1 +/// +/// Key characteristics: +/// - Emphasizes recent price movements more than traditional moving averages +/// - Maintains some influence from historical data +/// - Uses convolution for efficient calculation +/// - Provides better endpoint preservation than simple moving averages +/// +/// Implementation: +/// Original implementation based on convolution principles +/// + public class Epma : AbstractBase { private readonly int _period; private readonly Convolution _convolution; + /// The number of data points used in the EPMA calculation. + /// Thrown when period is less than 1. public Epma(int period) { if (period < 1) @@ -18,6 +43,8 @@ public class Epma : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of data points used in the EPMA calculation. public Epma(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -60,6 +87,11 @@ public class Epma : AbstractBase return result; } + /// + /// Generates the convolution kernel for the EPMA calculation. + /// + /// The period for which to generate the kernel. + /// An array of normalized weights for the convolution operation. public static double[] GenerateKernel(int period) { double[] kernel = new double[period]; @@ -79,4 +111,4 @@ public class Epma : AbstractBase return kernel; } -} \ No newline at end of file +} diff --git a/lib/averages/Frama.cs b/lib/averages/Frama.cs index 223b1458..9e509b82 100644 --- a/lib/averages/Frama.cs +++ b/lib/averages/Frama.cs @@ -2,6 +2,29 @@ using System; namespace QuanTAlib; +/// +/// FRAMA: Fractal Adaptive Moving Average +/// An adaptive moving average that adjusts its smoothing factor based on the fractal dimension +/// of the price series. FRAMA automatically adapts to market conditions, becoming more responsive +/// during trends and more stable during sideways markets. +/// +/// +/// The FRAMA algorithm works by: +/// 1. Calculating the fractal dimension of the price series +/// 2. Using this dimension to determine the optimal alpha (smoothing factor) +/// 3. Applying an EMA with the adaptive alpha +/// +/// Key characteristics: +/// - Self-adaptive to market conditions +/// - Reduces lag during trending periods +/// - Increases smoothing during sideways markets +/// - Uses fractal geometry principles for market analysis +/// +/// Sources: +/// John Ehlers - "FRAMA: A Trend-Following Indicator" +/// https://www.mesasoftware.com/papers/FRAMA.pdf +/// + public class Frama : AbstractBase { private readonly int _period; @@ -9,6 +32,8 @@ public class Frama : AbstractBase private double _lastFrama; private double _prevLastFrama; + /// The number of periods used for fractal dimension calculation. Must be at least 2. + /// Thrown when period is less than 2. public Frama(int period) { if (period < 2) @@ -19,6 +44,8 @@ public class Frama : AbstractBase WarmupPeriod = period; } + /// The data source object that publishes updates. + /// The number of periods used for fractal dimension calculation. public Frama(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -99,4 +126,4 @@ public class Frama : AbstractBase { return _lastFrama; } -} \ No newline at end of file +} diff --git a/lib/averages/Fwma.cs b/lib/averages/Fwma.cs index f5644d6a..98ee1bb1 100644 --- a/lib/averages/Fwma.cs +++ b/lib/averages/Fwma.cs @@ -1,9 +1,35 @@ +using System; namespace QuanTAlib; +/// +/// FWMA: Fibonacci Weighted Moving Average +/// A moving average that uses Fibonacci numbers as weights in its calculation. The weights +/// are arranged in reverse order so that recent prices receive higher weights corresponding +/// to larger Fibonacci numbers. +/// +/// +/// The FWMA calculation process: +/// 1. Generates a Fibonacci sequence up to the specified period +/// 2. Reverses the sequence to give higher weights to recent prices +/// 3. Normalizes the weights to sum to 1 +/// 4. Applies the weights through convolution +/// +/// Key characteristics: +/// - Uses Fibonacci sequence for weight distribution +/// - Recent prices receive higher weights +/// - Natural progression of weights based on the golden ratio +/// - Implemented using efficient convolution operations +/// +/// Implementation: +/// Original implementation based on Fibonacci sequence principles +/// + public class Fwma : AbstractBase { private readonly Convolution _convolution; + /// The number of data points used in the FWMA calculation. + /// Thrown when period is less than 1. public Fwma(int period) { if (period < 1) @@ -16,12 +42,19 @@ public class Fwma : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of data points used in the FWMA calculation. public Fwma(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + /// + /// Generates the Fibonacci-based convolution kernel for the FWMA calculation. + /// + /// The period for which to generate the kernel. + /// An array of normalized Fibonacci-based weights for the convolution operation. public static double[] GenerateKernel(int period) { double[] kernel = new double[period]; @@ -78,4 +111,4 @@ public class Fwma : AbstractBase return result; } -} \ No newline at end of file +} diff --git a/lib/averages/Gma.cs b/lib/averages/Gma.cs index 4f003f5c..74fafc08 100644 --- a/lib/averages/Gma.cs +++ b/lib/averages/Gma.cs @@ -1,9 +1,35 @@ +using System; namespace QuanTAlib; +/// +/// GMA: Gaussian Moving Average +/// A moving average that uses weights based on the Gaussian (normal) distribution curve. +/// This creates a smooth, bell-shaped weighting scheme that gives maximum weight to the +/// center of the period and gradually decreasing weights towards the edges. +/// +/// +/// The GMA calculation process: +/// 1. Creates a Gaussian distribution of weights centered on the period +/// 2. Normalizes the weights to sum to 1 +/// 3. Applies the weights through convolution +/// +/// Key characteristics: +/// - Smooth, symmetric weight distribution +/// - Natural bell curve weighting +/// - Reduces noise while preserving signal characteristics +/// - Less sensitive to outliers than simple moving averages +/// - Implemented using efficient convolution operations +/// +/// Implementation: +/// Based on Gaussian distribution principles from statistics +/// + public class Gma : AbstractBase { private readonly Convolution _convolution; + /// The number of data points used in the GMA calculation. + /// Thrown when period is less than 1. public Gma(int period) { if (period < 1) @@ -16,12 +42,20 @@ public class Gma : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of data points used in the GMA calculation. public Gma(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + /// + /// Generates the Gaussian-based convolution kernel for the GMA calculation. + /// + /// The period for which to generate the kernel. + /// The standard deviation parameter controlling the spread of the Gaussian curve. Default is 1.0. + /// An array of normalized Gaussian-based weights for the convolution operation. public static double[] GenerateKernel(int period, double sigma = 1.0) { double[] kernel = new double[period]; @@ -71,4 +105,4 @@ public class Gma : AbstractBase return result; } -} \ No newline at end of file +} diff --git a/lib/averages/Hma.cs b/lib/averages/Hma.cs index 220b419d..5b78c796 100644 --- a/lib/averages/Hma.cs +++ b/lib/averages/Hma.cs @@ -1,9 +1,37 @@ +using System; namespace QuanTAlib; +/// +/// HMA: Hull Moving Average +/// A moving average designed by Alan Hull to reduce lag while maintaining smoothness. +/// It combines weighted moving averages of different periods to achieve better +/// responsiveness to price changes while minimizing noise. +/// +/// +/// The HMA calculation process: +/// 1. Calculate WMA with period n/2 +/// 2. Calculate WMA with period n +/// 3. Calculate difference: 2*WMA(n/2) - WMA(n) +/// 4. Apply final WMA with period sqrt(n) to the difference +/// +/// Key characteristics: +/// - Significantly reduced lag compared to traditional moving averages +/// - Maintains smoothness despite the reduced lag +/// - Responds more quickly to price changes +/// - Better at identifying trend changes +/// - Uses weighted moving averages for all calculations +/// +/// Sources: +/// Alan Hull - "Better Trading with Hull Moving Average" +/// https://alanhull.com/hull-moving-average +/// + public class Hma : AbstractBase { private readonly Convolution _wmaHalf, _wmaFull, _wmaFinal; + /// The number of data points used in the HMA calculation. Must be at least 2. + /// Thrown when period is less than 2. public Hma(int period) { if (period < 2) @@ -19,12 +47,19 @@ public class Hma : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of data points used in the HMA calculation. public Hma(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + /// + /// Generates the weighted moving average kernel for the HMA calculation. + /// + /// The period for which to generate the kernel. + /// An array of linearly weighted values for the convolution operation. private static double[] GenerateWmaKernel(int period) { double[] kernel = new double[period]; @@ -72,4 +107,4 @@ public class Hma : AbstractBase IsHot = _index >= WarmupPeriod; return result; } -} \ No newline at end of file +} diff --git a/lib/averages/Htit.cs b/lib/averages/Htit.cs index 487a89f5..0c911f88 100644 --- a/lib/averages/Htit.cs +++ b/lib/averages/Htit.cs @@ -1,8 +1,32 @@ -//not working yet -//TODO fails consistency test - +using System; namespace QuanTAlib; +/// +/// HTIT: Hilbert Transform Instantaneous Trendline +/// A sophisticated moving average that uses the Hilbert Transform to identify the dominant cycle +/// period in price data and create a smooth trend line. It adapts to the market's natural cycles +/// and provides a dynamic moving average. +/// +/// +/// The HTIT calculation process: +/// 1. Uses a Hilbert Transform to decompose price into in-phase and quadrature components +/// 2. Employs a homodyne discriminator to determine the dominant cycle period +/// 3. Applies smoothing based on the detected cycle period +/// 4. Creates a trend line that automatically adapts to market cycles +/// +/// Key characteristics: +/// - Automatically adapts to market cycles +/// - Reduces lag by using cycle analysis +/// - Complex signal processing for better trend identification +/// - Combines multiple digital signal processing techniques +/// +/// Sources: +/// John Ehlers - "Cycle Analytics for Traders" +/// +/// Note: This implementation is currently under development and may not pass +/// all consistency tests. +/// + public class Htit : AbstractBase { private readonly CircularBuffer _priceBuffer = new(7); @@ -21,12 +45,19 @@ public class Htit : AbstractBase private double _lastPd = 0; private double _p_lastPd = 0; + /// + /// Initializes a new instance of the Htit class. + /// public Htit() { Name = "Htit"; WarmupPeriod = 12; } + /// + /// Initializes a new instance of the Htit class with a specified source. + /// + /// The data source object that publishes updates. public Htit(object source) : this() { var pubEvent = source.GetType().GetEvent("Pub"); @@ -45,6 +76,7 @@ public class Htit : AbstractBase _lastPd = _p_lastPd; } } + protected override double Calculation() { ManageState(Input.IsNew); @@ -141,4 +173,4 @@ public class Htit : AbstractBase return pr; } -} \ No newline at end of file +} diff --git a/lib/averages/Hwma.cs b/lib/averages/Hwma.cs index c2473349..20c67659 100644 --- a/lib/averages/Hwma.cs +++ b/lib/averages/Hwma.cs @@ -1,5 +1,33 @@ +using System; namespace QuanTAlib; +/// +/// HWMA: Holt-Winters Moving Average +/// A triple exponential smoothing method that incorporates level (F), velocity (V), and +/// acceleration (A) components to create a responsive yet smooth moving average. This +/// implementation uses optimized smoothing factors for each component. +/// +/// +/// The HWMA calculation process: +/// 1. Updates the level (F) component using alpha smoothing +/// 2. Updates the velocity (V) component using beta smoothing +/// 3. Updates the acceleration (A) component using gamma smoothing +/// 4. Combines all components for final value: F + V + 0.5A +/// +/// Key characteristics: +/// - Adapts to both trends and acceleration in price movement +/// - Three separate smoothing factors for fine-tuned control +/// - More responsive to changes than simple moving averages +/// - Handles both linear and non-linear trends +/// +/// Implementation: +/// Based on Holt-Winters triple exponential smoothing principles +/// with optimized default parameters: +/// - Alpha (nA) = 2/(period + 1) +/// - Beta (nB) = 1/period +/// - Gamma (nC) = 1/period +/// + public class Hwma : AbstractBase { private readonly int _period; @@ -7,14 +35,23 @@ public class Hwma : AbstractBase private double _pF, _pV, _pA; private double _ppF, _ppV, _ppA; + /// The number of data points used in the HWMA calculation. public Hwma(int period) : this(period, 2.0 / (1 + period), 1.0 / period, 1.0 / period) { } + /// Alpha smoothing factor for the level component. + /// Beta smoothing factor for the velocity component. + /// Gamma smoothing factor for the acceleration component. public Hwma(double nA, double nB, double nC) : this((int)((2 - nA) / nA), nA, nB, nC) { } + /// The number of data points used in the HWMA calculation. + /// Alpha smoothing factor for the level component. + /// Beta smoothing factor for the velocity component. + /// Gamma smoothing factor for the acceleration component. + /// Thrown when period is less than 1. public Hwma(int period, double nA, double nB, double nC) { if (period < 1) @@ -30,6 +67,8 @@ public class Hwma : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of data points used in the HWMA calculation. public Hwma(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -58,7 +97,6 @@ public class Hwma : AbstractBase _pF = _ppF; _pV = _ppV; _pA = _ppA; - } } @@ -93,4 +131,4 @@ public class Hwma : AbstractBase IsHot = _index >= WarmupPeriod; return hwma; } -} \ No newline at end of file +} diff --git a/lib/averages/Jma.cs b/lib/averages/Jma.cs index 0d3777cb..e250ca15 100644 --- a/lib/averages/Jma.cs +++ b/lib/averages/Jma.cs @@ -1,9 +1,31 @@ -/// -/// Represents a Jurik Moving Average, based on known and reverse-engineered insights -/// - +using System; namespace QuanTAlib; +/// +/// JMA: Jurik Moving Average +/// A sophisticated moving average that combines adaptive volatility measurement with +/// phase-shifted smoothing. JMA provides excellent noise reduction while maintaining +/// responsiveness to significant price movements. +/// +/// +/// The JMA calculation process: +/// 1. Calculates adaptive volatility bands +/// 2. Uses volatility to adjust smoothing parameters +/// 3. Applies phase-shifted smoothing for lag reduction +/// 4. Combines multiple smoothing stages for final output +/// +/// Key characteristics: +/// - Adaptive smoothing based on price volatility +/// - Phase-shifting to reduce lag +/// - Excellent noise reduction +/// - Maintains responsiveness to significant moves +/// - Provides volatility bands as additional outputs +/// +/// Implementation: +/// Based on known and reverse-engineered insights from Jurik Research +/// Original work by Mark Jurik +/// + public class Jma : AbstractBase { private readonly double _period; @@ -18,7 +40,6 @@ public class Jma : AbstractBase private double _prevMa1, _prevDet0, _prevDet1, _prevJma, _p_prevMa1, _p_prevDet0, _p_prevDet1, _p_prevJma; private double _vSum, _p_vSum; - public double UpperBand { get; set; } public double LowerBand { get; set; } public double Volty { get; set; } @@ -27,11 +48,11 @@ public class Jma : AbstractBase /// /// Initializes a new instance of the Jma class with the specified parameters. /// - /// The period over which to calculate the Jvolty. - /// The phase parameter for the JMA-style calculation. - /// - /// Thrown when period is less than 1. - /// + /// The period over which to calculate the JMA. + /// The phase parameter (-100 to +100) controlling lag compensation. + /// The factor controlling volatility adaptation (default 0.45). + /// The size of the volatility buffer (default 10). + /// Thrown when period is less than 1. public Jma(int period, int phase = 0, double factor = 0.45, int buffer = 10) { if (period < 1) @@ -51,20 +72,19 @@ public class Jma : AbstractBase } /// - /// Initializes a new instance of the Jvolty class with the specified source and parameters. + /// Initializes a new instance of the Jma class with a specified source. /// - /// The source object to subscribe to for value updates. - /// The period over which to calculate the Jvolty. - /// The phase parameter for the JMA-style calculation. + /// The data source object that publishes updates. + /// The period over which to calculate the JMA. + /// The phase parameter (-100 to +100) controlling lag compensation. + /// The factor controlling volatility adaptation (default 0.45). + /// The size of the volatility buffer (default 10). public Jma(object source, int period, int phase = 0, double factor = 0.45, int buffer = 10) : this(period, phase, factor, buffer) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - /// - /// Initializes the Jma instance by setting up the initial state. - /// public override void Init() { base.Init(); @@ -76,10 +96,6 @@ public class Jma : AbstractBase _vsumBuff.Clear(); } - /// - /// Manages the state of the Jma instance based on whether a new value is being processed. - /// - /// Indicates whether the current input is a new value. protected override void ManageState(bool isNew) { if (isNew) @@ -105,12 +121,6 @@ public class Jma : AbstractBase } } - /// - /// Performs the Jma calculation for the current value. - /// - /// - /// The calculated Jma value for the current input. - /// protected override double Calculation() { ManageState(Input.IsNew); @@ -141,10 +151,10 @@ public class Jma : AbstractBase _lowerBand = (del2 <= 0) ? price : price - (Kv * del2); double _alpha = Math.Pow(_beta, pow2); - double ma1 = Input.Value + _alpha * (_prevMa1 - Input.Value); //original: (1 - _alpha) * Input.Value + _alpha * _prevMa1; + double ma1 = Input.Value + _alpha * (_prevMa1 - Input.Value); _prevMa1 = ma1; - double det0 = price + _beta * (_prevDet0 - price + ma1) - ma1; //original: (price - ma1) * (1 - _beta) + _beta * _prevDet0; + double det0 = price + _beta * (_prevDet0 - price + ma1) - ma1; _prevDet0 = det0; double ma2 = ma1 + _phase * det0; diff --git a/lib/averages/Kama.cs b/lib/averages/Kama.cs index e509c5e3..7c15a827 100644 --- a/lib/averages/Kama.cs +++ b/lib/averages/Kama.cs @@ -1,5 +1,30 @@ +using System; namespace QuanTAlib; +/// +/// KAMA: Kaufman's Adaptive Moving Average +/// An adaptive moving average that adjusts its smoothing based on market efficiency. +/// KAMA responds quickly during trending periods and becomes more stable during +/// sideways or choppy markets. +/// +/// +/// The KAMA calculation process: +/// 1. Calculates the Efficiency Ratio (ER) to measure market noise +/// 2. Uses ER to determine the optimal smoothing between fast and slow constants +/// 3. Applies the adaptive smoothing to create the moving average +/// +/// Key characteristics: +/// - Self-adaptive to market conditions +/// - Fast response during trends +/// - Stable during sideways markets +/// - Uses market efficiency for smoothing adjustment +/// - Reduces whipsaws in choppy markets +/// +/// Sources: +/// Perry Kaufman - "Smarter Trading" +/// https://www.investopedia.com/terms/k/kaufmansadaptivemovingaverage.asp +/// + public class Kama : AbstractBase { private readonly int _period; @@ -7,6 +32,10 @@ public class Kama : AbstractBase private CircularBuffer? _buffer; private double _lastKama, _p_lastKama; + /// The number of periods used to calculate the Efficiency Ratio. + /// The number of periods for the fastest EMA response (default 2). + /// The number of periods for the slowest EMA response (default 30). + /// Thrown when period is less than 1. public Kama(int period, int fast = 2, int slow = 30) { if (period < 1) @@ -21,6 +50,10 @@ public class Kama : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of periods used to calculate the Efficiency Ratio. + /// The number of periods for the fastest EMA response (default 2). + /// The number of periods for the slowest EMA response (default 30). public Kama(object source, int period, int fast = 2, int slow = 30) : this(period, fast, slow) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -80,4 +113,4 @@ public class Kama : AbstractBase return kama; } -} \ No newline at end of file +} diff --git a/lib/averages/Ltma.cs b/lib/averages/Ltma.cs index e0aca68b..910322ab 100644 --- a/lib/averages/Ltma.cs +++ b/lib/averages/Ltma.cs @@ -1,6 +1,30 @@ +using System; namespace QuanTAlib; -// https://www.mesasoftware.com/papers/TimeWarp.pdf +/// +/// LTMA: Laguerre Time Moving Average +/// A sophisticated moving average that uses Laguerre polynomials to create a time-based +/// filter. This approach provides excellent noise reduction while maintaining +/// responsiveness to price changes. +/// +/// +/// The LTMA calculation process: +/// 1. Applies a cascade of four Laguerre filters +/// 2. Each filter stage provides additional smoothing +/// 3. Combines the filtered outputs with optimal weights +/// 4. Produces a smooth output with minimal lag +/// +/// Key characteristics: +/// - Time-based filtering using Laguerre polynomials +/// - Excellent noise reduction +/// - Maintains good responsiveness +/// - Single parameter (gamma) controls smoothing +/// - Computationally efficient +/// +/// Sources: +/// John Ehlers - "Time Warp - Without Space Travel" +/// https://www.mesasoftware.com/papers/TimeWarp.pdf +/// public class Ltma : AbstractBase { @@ -8,8 +32,13 @@ public class Ltma : AbstractBase private double _prevL0, _prevL1, _prevL2, _prevL3; private double _p_prevL0, _p_prevL1, _p_prevL2, _p_prevL3; + /// + /// Gets the gamma parameter value used in the Laguerre filter. + /// public double Gamma => _gamma; + /// The damping factor (0 to 1) controlling the smoothing. Lower values provide more smoothing. + /// Thrown when gamma is not between 0 and 1. public Ltma(double gamma = 0.1) { if (gamma < 0 || gamma > 1) @@ -20,6 +49,8 @@ public class Ltma : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The damping factor (0 to 1) controlling the smoothing. public Ltma(object source, double gamma = 0.1) : this(gamma) { var pubEvent = source.GetType().GetEvent("Pub"); diff --git a/lib/averages/Maaf.cs b/lib/averages/Maaf.cs index 2b31ddf0..b3ddc808 100644 --- a/lib/averages/Maaf.cs +++ b/lib/averages/Maaf.cs @@ -1,8 +1,33 @@ +using System; +using System.Linq; namespace QuanTAlib; -// https://efs.kb.esignal.com/hc/en-us/articles/6362791434395-2005-Mar-The-Secret-Behind-The-Filter-MedianAdaptiveFilter-efs - -//TODO Fix initial values +/// +/// MAAF: Median Adaptive Average Filter +/// A sophisticated moving average that combines median filtering with adaptive smoothing +/// to provide robust noise reduction while maintaining signal fidelity. The filter +/// automatically adjusts its length based on market conditions. +/// +/// +/// The MAAF calculation process: +/// 1. Applies initial smoothing using weighted moving average +/// 2. Uses median filtering to remove outliers +/// 3. Adaptively adjusts filter length based on price deviation +/// 4. Applies final EMA smoothing with adaptive period +/// +/// Key characteristics: +/// - Combines median and exponential filtering +/// - Adaptive period adjustment +/// - Robust noise reduction +/// - Preserves significant price movements +/// - Reduces impact of outliers +/// +/// Sources: +/// John F. Ehlers - "The Secret Behind The Filter" +/// https://efs.kb.esignal.com/hc/en-us/articles/6362791434395-2005-Mar-The-Secret-Behind-The-Filter-MedianAdaptiveFilter-efs +/// +/// Note: Initial values handling is currently under development. +/// public class Maaf : AbstractBase { @@ -14,6 +39,8 @@ public class Maaf : AbstractBase private readonly int _period; + /// The initial period for the filter (default 39). + /// The threshold for adaptive adjustment (default 0.002). public Maaf(int period = 39, double threshold = 0.002) { _period = period; @@ -25,6 +52,9 @@ public class Maaf : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The initial period for the filter (default 39). + /// The threshold for adaptive adjustment (default 0.002). public Maaf(object source, int period = 39, double threshold = 0.002) : this(period, threshold) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -70,7 +100,6 @@ public class Maaf : AbstractBase double smooth = (_priceBuffer[^1] + (2 * _priceBuffer[^2]) + (2 * _priceBuffer[^3]) + _priceBuffer[^4]) / 6; _smoothBuffer.Add(smooth, Input.IsNew); - if (_smoothBuffer.Count < _period) { return smooth; diff --git a/lib/averages/Mama.cs b/lib/averages/Mama.cs index 3af81ddb..74e82a73 100644 --- a/lib/averages/Mama.cs +++ b/lib/averages/Mama.cs @@ -1,5 +1,32 @@ +using System; namespace QuanTAlib; +/// +/// MAMA: MESA Adaptive Moving Average +/// A highly sophisticated adaptive moving average that uses the MESA (Maximum Entropy +/// Spectral Analysis) algorithm to detect market cycles and adjust its smoothing +/// accordingly. MAMA provides both a faster (MAMA) and slower (FAMA) moving average. +/// +/// +/// The MAMA calculation process: +/// 1. Uses Hilbert Transform to decompose price into phase and amplitude +/// 2. Calculates the dominant cycle period using phase analysis +/// 3. Determines phase position and rate of change +/// 4. Adapts smoothing based on phase changes +/// 5. Generates both MAMA and FAMA (Following Adaptive Moving Average) +/// +/// Key characteristics: +/// - Highly adaptive to market conditions +/// - Provides two synchronized moving averages +/// - Uses cycle analysis for adaptation +/// - Excellent at identifying trend changes +/// - Combines multiple signal processing techniques +/// +/// Sources: +/// John Ehlers - "MESA Adaptive Moving Averages" +/// https://www.mesasoftware.com/papers/MAMA.pdf +/// + public class Mama : AbstractBase { private readonly double _fastLimit, _slowLimit; @@ -8,8 +35,13 @@ public class Mama : AbstractBase private double _prevMama, _prevFama, _sumPr; private double _p_prevMama, _p_prevFama, _p_sumPr; + /// + /// Gets the Following Adaptive Moving Average (FAMA) value. + /// public TValue Fama { get; private set; } + /// The maximum adaptation speed (default 0.5). + /// The minimum adaptation speed (default 0.05). public Mama(double fastLimit = 0.5, double slowLimit = 0.05) { Fama = new TValue(); @@ -30,6 +62,9 @@ public class Mama : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The maximum adaptation speed (default 0.5). + /// The minimum adaptation speed (default 0.05). public Mama(object source, double fastLimit = 0.5, double slowLimit = 0.05) : this(fastLimit, slowLimit) { var pubEvent = source.GetType().GetEvent("Pub"); diff --git a/lib/averages/Mgdi.cs b/lib/averages/Mgdi.cs index 27d8dd4a..70ec070c 100644 --- a/lib/averages/Mgdi.cs +++ b/lib/averages/Mgdi.cs @@ -1,10 +1,39 @@ +using System; namespace QuanTAlib; +/// +/// MGDI: Modified Geometric Decay Index +/// A moving average that uses geometric decay with a ratio-based adjustment factor. +/// The decay rate is modified based on the ratio between current and previous values, +/// allowing for adaptive smoothing based on price movement magnitude. +/// +/// +/// The MGDI calculation process: +/// 1. Calculates ratio between current and previous values +/// 2. Uses ratio to modify the geometric decay rate +/// 3. Applies modified decay to smooth the data +/// 4. Adjusts smoothing based on K-factor parameter +/// +/// Key characteristics: +/// - Geometric decay-based smoothing +/// - Adaptive to price movement magnitude +/// - Adjustable smoothing via K-factor +/// - More responsive to large price changes +/// - Maintains smoothness during small fluctuations +/// +/// Implementation: +/// Based on geometric decay principles with ratio-based modification +/// + public class Mgdi : AbstractBase { private readonly int _period; private readonly double _kFactor; private double _prevMd, _p_prevMd; + + /// The number of periods used in the MGDI calculation. + /// The K-factor controlling the decay rate adjustment (default 0.6). + /// Thrown when period or kFactor is less than or equal to 0. public Mgdi(int period, double kFactor = 0.6) { if (period <= 0) @@ -22,6 +51,9 @@ public class Mgdi : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of periods used in the MGDI calculation. + /// The K-factor controlling the decay rate adjustment (default 0.6). public Mgdi(object source, int period, double kFactor = 0.6) : this(period, kFactor) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -67,4 +99,4 @@ public class Mgdi : AbstractBase IsHot = _index >= _period; return _prevMd; } -} \ No newline at end of file +} diff --git a/lib/averages/Mma.cs b/lib/averages/Mma.cs index cc02e1bc..99d92ab3 100644 --- a/lib/averages/Mma.cs +++ b/lib/averages/Mma.cs @@ -1,11 +1,38 @@ +using System; namespace QuanTAlib; +/// +/// MMA: Modified Moving Average +/// A moving average that combines a simple moving average with a weighted component +/// to provide a balanced smoothing effect. The weighting scheme emphasizes central +/// values while maintaining overall data representation. +/// +/// +/// The MMA calculation process: +/// 1. Calculates the simple moving average component (T/period) +/// 2. Calculates a weighted sum with symmetric weights around the center +/// 3. Combines both components using the formula: SMA + 6*WeightedSum/((period+1)*period) +/// +/// Key characteristics: +/// - Combines simple and weighted moving averages +/// - Symmetric weighting around the center +/// - Better balance between smoothing and responsiveness +/// - Reduces lag compared to simple moving average +/// - Maintains stability through dual-component approach +/// +/// Implementation: +/// Based on modified moving average principles combining +/// simple and weighted components for optimal smoothing +/// + public class Mma : AbstractBase { private readonly int _period; private readonly CircularBuffer _buffer; private double _lastMma; + /// The number of periods used in the MMA calculation. Must be at least 2. + /// Thrown when period is less than 2. public Mma(int period) { if (period < 2) @@ -19,6 +46,8 @@ public class Mma : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of periods used in the MMA calculation. public Mma(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -61,6 +90,11 @@ public class Mma : AbstractBase return _lastMma; } + /// + /// Calculates the weighted sum component of the MMA. + /// The weights are symmetric around the center, decreasing linearly from the center outward. + /// + /// The weighted sum of the data points. private double CalculateWeightedSum() { double sum = 0; diff --git a/lib/averages/Pwma.cs b/lib/averages/Pwma.cs index fc2aac76..73c27470 100644 --- a/lib/averages/Pwma.cs +++ b/lib/averages/Pwma.cs @@ -1,10 +1,39 @@ +using System; +using System.Linq; namespace QuanTAlib; +/// +/// PWMA: Pascal Weighted Moving Average +/// A moving average that uses Pascal's triangle coefficients as weights, providing +/// a natural distribution of weights that increases towards the center of the period. +/// This creates a smooth average with balanced emphasis on central values. +/// +/// +/// The PWMA calculation process: +/// 1. Generates weights using Pascal's triangle coefficients +/// 2. Normalizes the weights to sum to 1 +/// 3. Applies the weights through convolution +/// 4. Adjusts for partial periods during warmup +/// +/// Key characteristics: +/// - Natural weight distribution from Pascal's triangle +/// - Symmetric weighting around the center +/// - Smooth response to price changes +/// - Balanced between recent and historical data +/// - Implemented using efficient convolution operations +/// +/// Implementation: +/// Based on Pascal's triangle principles for weight generation +/// Uses convolution for efficient calculation +/// + public class Pwma : AbstractBase { private readonly int _period; private readonly Convolution _convolution; + /// The number of data points used in the PWMA calculation. + /// Thrown when period is less than 1. public Pwma(int period) { if (period < 1) @@ -18,6 +47,8 @@ public class Pwma : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of data points used in the PWMA calculation. public Pwma(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -60,6 +91,11 @@ public class Pwma : AbstractBase return result; } + /// + /// Generates the Pascal's triangle-based convolution kernel for the PWMA calculation. + /// + /// The period for which to generate the kernel. + /// An array of normalized Pascal's triangle-based weights for the convolution operation. public static double[] GenerateKernel(int period) { double[] kernel = new double[period]; diff --git a/lib/averages/Qema.cs b/lib/averages/Qema.cs index d09e6ef0..cdf08f82 100644 --- a/lib/averages/Qema.cs +++ b/lib/averages/Qema.cs @@ -1,11 +1,42 @@ +using System; namespace QuanTAlib; +/// +/// QEMA: Quadruple Exponential Moving Average +/// A sophisticated moving average that applies four exponential moving averages in sequence +/// and combines them using a specific formula to reduce lag while maintaining smoothness. +/// The final combination is: 4*EMA1 - 6*EMA2 + 4*EMA3 - EMA4 +/// +/// +/// The QEMA calculation process: +/// 1. Applies first EMA to price data +/// 2. Applies second EMA to result of first EMA +/// 3. Applies third EMA to result of second EMA +/// 4. Applies fourth EMA to result of third EMA +/// 5. Combines results using the formula: 4*EMA1 - 6*EMA2 + 4*EMA3 - EMA4 +/// +/// Key characteristics: +/// - Multiple EMA smoothing stages +/// - Reduced lag through combination formula +/// - Customizable smoothing factors for each EMA +/// - Better noise reduction than single EMA +/// - Maintains responsiveness to significant moves +/// +/// Implementation: +/// Based on quadruple exponential smoothing principles +/// with optimized combination formula +/// + public class Qema : AbstractBase { - private readonly Ema _ema1, _ema2, _ema3, _ema4; private double _lastQema, _p_lastQema; + /// Smoothing factor for first EMA (default 0.2). + /// Smoothing factor for second EMA (default 0.2). + /// Smoothing factor for third EMA (default 0.2). + /// Smoothing factor for fourth EMA (default 0.2). + /// Thrown when any k value is less than or equal to 0. public Qema(double k1 = 0.2, double k2 = 0.2, double k3 = 0.2, double k4 = 0.2) { if (k1 <= 0 || k2 <= 0 || k3 <= 0 || k4 <= 0) @@ -25,6 +56,11 @@ public class Qema : AbstractBase Init(); } + /// The data source object that publishes updates. + /// Smoothing factor for first EMA. + /// Smoothing factor for second EMA. + /// Smoothing factor for third EMA. + /// Smoothing factor for fourth EMA. public Qema(object source, double k1, double k2, double k3, double k4) : this(k1, k2, k3, k4) { @@ -66,4 +102,4 @@ public class Qema : AbstractBase IsHot = _index >= WarmupPeriod; return _lastQema; } -} \ No newline at end of file +} diff --git a/lib/averages/Rema.cs b/lib/averages/Rema.cs index afa22046..b8420a3d 100644 --- a/lib/averages/Rema.cs +++ b/lib/averages/Rema.cs @@ -1,6 +1,29 @@ +using System; namespace QuanTAlib; -//https://user42.tuxfamily.org/chart/manual/Regularized-Exponential-Moving-Average.html +/// +/// REMA: Regularized Exponential Moving Average +/// A modified exponential moving average that includes a regularization term to reduce +/// noise and improve trend following. The regularization helps to smooth the output +/// while maintaining responsiveness to significant price movements. +/// +/// +/// The REMA calculation process: +/// 1. Uses standard EMA smoothing with adaptive alpha +/// 2. Adds regularization term based on previous values +/// 3. Balances new and regularized terms using lambda parameter +/// 4. Provides smoother output than standard EMA +/// +/// Key characteristics: +/// - Improved noise reduction through regularization +/// - Better trend following than standard EMA +/// - Adjustable regularization via lambda parameter +/// - Adaptive alpha based on period +/// - Reduced whipsaws in choppy markets +/// +/// Sources: +/// https://user42.tuxfamily.org/chart/manual/Regularized-Exponential-Moving-Average.html +/// public class Rema : AbstractBase { @@ -9,9 +32,19 @@ public class Rema : AbstractBase private double _lastRema, _prevRema; private double _savedLastRema, _savedPrevRema; + /// + /// Gets the period used in the REMA calculation. + /// public int Period => _period; + + /// + /// Gets the lambda (regularization) parameter value. + /// public double Lambda => _lambda; + /// The number of periods used in the REMA calculation. + /// The regularization parameter (default 0.5). Higher values increase smoothing. + /// Thrown when period is less than 1 or lambda is negative. public Rema(int period, double lambda = 0.5) { if (period < 1) @@ -25,11 +58,16 @@ public class Rema : AbstractBase WarmupPeriod = period; Init(); } + + /// The data source object that publishes updates. + /// The number of periods used in the REMA calculation. + /// The regularization parameter (default 0.5). public Rema(object source, int period, double lambda = 0.5) : this(period, lambda) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + public override void Init() { base.Init(); @@ -79,4 +117,4 @@ public class Rema : AbstractBase IsHot = _index >= WarmupPeriod; return _lastRema; } -} \ No newline at end of file +} diff --git a/lib/averages/Sinema.cs b/lib/averages/Sinema.cs index 6834dc64..64efde2e 100644 --- a/lib/averages/Sinema.cs +++ b/lib/averages/Sinema.cs @@ -1,9 +1,37 @@ +using System; namespace QuanTAlib; +/// +/// SINEMA: Sine-weighted Exponential Moving Average +/// A moving average that uses sine function-based weights to create a natural +/// distribution of importance across the period. The weights follow a sine curve, +/// providing smooth transitions and natural emphasis on different parts of the data. +/// +/// +/// The SINEMA calculation process: +/// 1. Generates weights using sine function over the period +/// 2. Normalizes weights to sum to 1 +/// 3. Applies weights through convolution +/// 4. Produces smooth output with natural weight distribution +/// +/// Key characteristics: +/// - Sine-based weight distribution +/// - Natural smoothing through trigonometric weights +/// - No sharp transitions in weight values +/// - Balanced emphasis across the period +/// - Implemented using efficient convolution operations +/// +/// Implementation: +/// Based on sine function principles for weight generation +/// Uses convolution for efficient calculation +/// + public class Sinema : AbstractBase { private readonly Convolution _convolution; + /// The number of data points used in the SINEMA calculation. + /// Thrown when period is less than 1. public Sinema(int period) { if (period < 1) @@ -16,6 +44,8 @@ public class Sinema : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of data points used in the SINEMA calculation. public Sinema(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -50,6 +80,11 @@ public class Sinema : AbstractBase return result; } + /// + /// Generates the sine-based convolution kernel for the SINEMA calculation. + /// + /// The period for which to generate the kernel. + /// An array of normalized sine-based weights for the convolution operation. public static double[] GenerateKernel(int period) { double[] kernel = new double[period]; @@ -70,4 +105,4 @@ public class Sinema : AbstractBase return kernel; } -} \ No newline at end of file +} diff --git a/lib/averages/Sma.cs b/lib/averages/Sma.cs index c0294bd0..6c797162 100644 --- a/lib/averages/Sma.cs +++ b/lib/averages/Sma.cs @@ -1,11 +1,38 @@ +using System; namespace QuanTAlib; +/// +/// SMA: Simple Moving Average +/// The most basic form of moving average, calculating the arithmetic mean over a +/// specified period. Each data point in the period has equal weight in the +/// calculation. +/// +/// +/// The SMA calculation process: +/// 1. Maintains a buffer of the last 'period' values +/// 2. Calculates arithmetic mean of all values in the buffer +/// 3. Updates buffer with new values in FIFO manner +/// +/// Key characteristics: +/// - Equal weight for all values in the period +/// - Simple and straightforward calculation +/// - Significant lag due to equal weighting +/// - Smooth output with good noise reduction +/// - Most basic form of trend following +/// +/// Sources: +/// https://www.investopedia.com/terms/s/sma.asp +/// https://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:moving_averages +/// + public class Sma : AbstractBase { // inherited _index // inherited _value private readonly CircularBuffer _buffer; + /// The number of data points used in the SMA calculation. + /// Thrown when period is less than 1. public Sma(int period) { if (period < 1) @@ -19,12 +46,13 @@ public class Sma : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of data points used in the SMA calculation. public Sma(object source, int period) : this(period: period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - //inhereted public void Sub(object source, in ValueEventArgs args) protected override void ManageState(bool isNew) { @@ -36,9 +64,9 @@ public class Sma : AbstractBase } /// - /// Core SMA calculation - using _buffer.Average + /// Performs the core SMA calculation using the circular buffer's average. /// - + /// The calculated SMA value. protected override double Calculation() { double result; @@ -49,4 +77,4 @@ public class Sma : AbstractBase IsHot = _index >= WarmupPeriod; return result; } -} \ No newline at end of file +} diff --git a/lib/averages/Smma.cs b/lib/averages/Smma.cs index 0ca8c403..f66262b0 100644 --- a/lib/averages/Smma.cs +++ b/lib/averages/Smma.cs @@ -1,13 +1,38 @@ using System; - namespace QuanTAlib; +/// +/// SMMA: Smoothed Moving Average +/// A modified moving average that gives more weight to recent prices while maintaining +/// a smooth output. It uses the previous SMMA value in its calculation, creating +/// a smoother line than traditional moving averages. +/// +/// +/// The SMMA calculation process: +/// 1. Uses SMA for initial value (first period points) +/// 2. For subsequent points, calculates: (prevSMMA * (period-1) + price) / period +/// 3. This creates a smoothed effect with reduced volatility +/// +/// Key characteristics: +/// - Smoother than traditional moving averages +/// - Reduced volatility in output +/// - Takes into account all previous prices +/// - Good for identifying overall trends +/// - Less lag than SMA but more than EMA +/// +/// Implementation: +/// Based on smoothed moving average principles with +/// initial SMA seeding for stability +/// + public class Smma : AbstractBase { private readonly int _period; private CircularBuffer? _buffer; private double _lastSmma, _p_lastSmma; + /// The number of data points used in the SMMA calculation. + /// Thrown when period is less than 1. public Smma(int period) { if (period < 1) @@ -20,6 +45,8 @@ public class Smma : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of data points used in the SMMA calculation. public Smma(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -47,7 +74,6 @@ public class Smma : AbstractBase } } - protected override double Calculation() { ManageState(Input.IsNew); @@ -75,4 +101,4 @@ public class Smma : AbstractBase return smma; } -} \ No newline at end of file +} diff --git a/lib/averages/T3.cs b/lib/averages/T3.cs index 2924fe95..49a00cb5 100644 --- a/lib/averages/T3.cs +++ b/lib/averages/T3.cs @@ -1,5 +1,31 @@ +using System; namespace QuanTAlib; +/// +/// T3: Tillson T3 Moving Average +/// A sophisticated moving average developed by Tim Tillson that applies six EMAs +/// in sequence with optimized coefficients. The T3 provides excellent smoothing +/// while maintaining responsiveness and minimal lag. +/// +/// +/// The T3 calculation process: +/// 1. Applies six EMAs in sequence +/// 2. Uses volume factor to determine optimal coefficients +/// 3. Combines EMAs using specific formula: c1*EMA6 + c2*EMA5 + c3*EMA4 + c4*EMA3 +/// 4. Coefficients are based on the volume factor parameter +/// +/// Key characteristics: +/// - Excellent smoothing with minimal lag +/// - Adjustable via volume factor parameter +/// - No overshooting like triple EMA +/// - Better noise reduction than traditional EMAs +/// - Maintains responsiveness to significant moves +/// +/// Sources: +/// Tim Tillson - "Better Moving Averages" +/// TASC Magazine, 1998 +/// + public class T3 : AbstractBase { private readonly int _period; @@ -10,6 +36,10 @@ public class T3 : AbstractBase private double _lastEma1, _lastEma2, _lastEma3, _lastEma4, _lastEma5, _lastEma6; private double _p_lastEma1, _p_lastEma2, _p_lastEma3, _p_lastEma4, _p_lastEma5, _p_lastEma6; + /// The number of periods used in each EMA calculation. + /// Volume factor controlling smoothing (default 0.7). + /// Whether to use SMA for initial values (default true). + /// Thrown when period is less than 1. public T3(int period, double vfactor = 0.7, bool useSma = true) { if (period < 1) @@ -35,11 +65,14 @@ public class T3 : AbstractBase _buffer5 = new(period); _buffer6 = new(period); - Name = $"T3({_period}, {_vfactor})"; Init(); } + /// The data source object that publishes updates. + /// The number of periods used in each EMA calculation. + /// Volume factor controlling smoothing (default 0.7). + /// Whether to use SMA for initial values (default true). public T3(object source, int period, double vfactor = 0.7, bool useSma = true) : this(period, vfactor, useSma) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -81,7 +114,6 @@ public class T3 : AbstractBase } } - protected override double Calculation() { ManageState(Input.IsNew); @@ -129,4 +161,4 @@ public class T3 : AbstractBase IsHot = _index >= WarmupPeriod; return t3; } -} \ No newline at end of file +} diff --git a/lib/averages/Tema.cs b/lib/averages/Tema.cs index 4d4abef7..1090cd2a 100644 --- a/lib/averages/Tema.cs +++ b/lib/averages/Tema.cs @@ -1,5 +1,31 @@ +using System; namespace QuanTAlib; +/// +/// TEMA: Triple Exponential Moving Average +/// A sophisticated moving average that applies three EMAs in sequence with a specific +/// combination formula to reduce lag while maintaining smoothness. The formula +/// 3*EMA1 - 3*EMA2 + EMA3 helps eliminate lag in trending markets. +/// +/// +/// The TEMA calculation process: +/// 1. Calculates first EMA of the price +/// 2. Calculates second EMA of the first EMA +/// 3. Calculates third EMA of the second EMA +/// 4. Combines using formula: 3*EMA1 - 3*EMA2 + EMA3 +/// +/// Key characteristics: +/// - Significantly reduced lag compared to single EMA +/// - Better response to trends than standard EMAs +/// - Maintains smoothness despite reduced lag +/// - More responsive than double EMA (DEMA) +/// - Uses compensator for early values +/// +/// Sources: +/// Patrick Mulloy - "Smoothing Data with Faster Moving Averages" +/// Technical Analysis of Stocks and Commodities, 1994 +/// + public class Tema : AbstractBase { private readonly int _period; @@ -8,6 +34,8 @@ public class Tema : AbstractBase private double _lastEma3, _p_lastEma3; private double _k, _e, _p_e; + /// The number of periods used in each EMA calculation. + /// Thrown when period is less than 1. public Tema(int period) { if (period < 1) @@ -21,6 +49,8 @@ public class Tema : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of periods used in each EMA calculation. public Tema(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -63,9 +93,7 @@ public class Tema : AbstractBase double _invE = (_e > 1e-10) ? 1 / (1 - _e) : 1; _ema1 = _k * (Input.Value - _lastEma1) + _lastEma1; - _ema2 = _k * (_ema1 * _invE - _lastEma2) + _lastEma2; - _ema3 = _k * (_ema2 * _invE - _lastEma3) + _lastEma3; double _tema = 3 * _ema1 * _invE - 3 * _ema2 * _invE + _ema3 * _invE; @@ -78,4 +106,4 @@ public class Tema : AbstractBase IsHot = _index >= WarmupPeriod; return result; } -} \ No newline at end of file +} diff --git a/lib/averages/Trima.cs b/lib/averages/Trima.cs index 7cfeb715..93e8a17b 100644 --- a/lib/averages/Trima.cs +++ b/lib/averages/Trima.cs @@ -1,9 +1,37 @@ +using System; namespace QuanTAlib; +/// +/// TRIMA: Triangular Moving Average +/// A moving average that uses triangular-shaped weights that increase linearly to +/// the middle of the period and then decrease linearly. This creates a smoother +/// output than simple moving averages. +/// +/// +/// The TRIMA calculation process: +/// 1. Generates triangular weights that peak at the center +/// 2. Weights increase linearly to middle point +/// 3. Weights decrease linearly from middle point +/// 4. Applies normalized weights through convolution +/// +/// Key characteristics: +/// - Smoother than simple moving average +/// - Natural emphasis on central values +/// - Reduced noise sensitivity +/// - Double smoothing effect +/// - Implemented using efficient convolution operations +/// +/// Sources: +/// https://www.investopedia.com/terms/t/triangularaverage.asp +/// Technical Analysis of Stocks & Commodities magazine +/// + public class Trima : AbstractBase { private readonly Convolution _convolution; + /// The number of data points used in the TRIMA calculation. + /// Thrown when period is less than 1. public Trima(int period) { if (period < 1) @@ -16,12 +44,19 @@ public class Trima : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of data points used in the TRIMA calculation. public Trima(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + /// + /// Generates the triangular-shaped convolution kernel for the TRIMA calculation. + /// + /// The period for which to generate the kernel. + /// An array of normalized triangular weights for the convolution operation. private static double[] GenerateKernel(int period) { double[] kernel = new double[period]; @@ -70,4 +105,4 @@ public class Trima : AbstractBase return result; } -} \ No newline at end of file +} diff --git a/lib/averages/Vidya.cs b/lib/averages/Vidya.cs index 539aced4..125b3e68 100644 --- a/lib/averages/Vidya.cs +++ b/lib/averages/Vidya.cs @@ -4,15 +4,43 @@ using System.Runtime.CompilerServices; namespace QuanTAlib; +/// +/// VIDYA: Variable Index Dynamic Average +/// An adaptive moving average that adjusts its smoothing based on the ratio of +/// short-term to long-term volatility. This allows the average to become more +/// responsive during volatile periods and more stable during quiet periods. +/// +/// +/// The VIDYA calculation process: +/// 1. Calculates standard deviation for short and long periods +/// 2. Uses ratio of short/long volatility to determine smoothing +/// 3. Applies variable smoothing factor to price data +/// 4. Adapts automatically to changing market conditions +/// +/// Key characteristics: +/// - Adaptive smoothing based on volatility +/// - More responsive during volatile periods +/// - More stable during quiet periods +/// - Uses standard deviation for volatility measurement +/// - Combines short and long-term market analysis +/// +/// Sources: +/// Tushar Chande - "Beyond Technical Analysis" +/// https://www.investopedia.com/terms/v/vidya.asp +/// + public class Vidya : AbstractBase { - private readonly int _longPeriod; private readonly double _alpha; private double _lastVIDYA, _p_lastVIDYA; private readonly CircularBuffer? _shortBuffer; private readonly CircularBuffer? _longBuffer; + /// The number of periods for short-term volatility calculation. + /// The number of periods for long-term volatility calculation (default is 4x shortPeriod). + /// The alpha parameter controlling the base smoothing factor (default 0.2). + /// Thrown when shortPeriod is less than 1. public Vidya(int shortPeriod, int longPeriod = 0, double alpha = 0.2) { if (shortPeriod < 1) @@ -28,6 +56,10 @@ public class Vidya : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of periods for short-term volatility calculation. + /// The number of periods for long-term volatility calculation (default is 4x shortPeriod). + /// The alpha parameter controlling the base smoothing factor (default 0.2). public Vidya(object source, int shortPeriod, int longPeriod = 0, double alpha = 0.2) : this(shortPeriod, longPeriod, alpha) { @@ -81,6 +113,11 @@ public class Vidya : AbstractBase return vidya; } + /// + /// Calculates the standard deviation of values in a circular buffer. + /// + /// The circular buffer containing the values. + /// The standard deviation of the values in the buffer. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double CalculateStdDev(CircularBuffer buffer) { @@ -88,4 +125,4 @@ public class Vidya : AbstractBase double sumSquaredDiff = buffer.Sum(x => Math.Pow(x - mean, 2)); return Math.Sqrt(sumSquaredDiff / buffer.Count); } -} \ No newline at end of file +} diff --git a/lib/averages/Wma.cs b/lib/averages/Wma.cs index 9497ba25..b3ae1167 100644 --- a/lib/averages/Wma.cs +++ b/lib/averages/Wma.cs @@ -1,10 +1,39 @@ +using System; namespace QuanTAlib; +/// +/// WMA: Weighted Moving Average +/// A moving average that assigns linearly decreasing weights to older data points. +/// The most recent price has the highest weight, and each older price receives +/// linearly less weight, creating a more responsive average than SMA. +/// +/// +/// The WMA calculation process: +/// 1. Assigns weights linearly decreasing with age +/// 2. Most recent price gets weight of period +/// 3. Each older price gets decremented weight +/// 4. Normalizes weights by sum of weights +/// 5. Applies weights through convolution +/// +/// Key characteristics: +/// - Linear weight distribution +/// - More responsive than SMA +/// - Less lag than SMA +/// - Emphasizes recent prices +/// - Implemented using efficient convolution operations +/// +/// Sources: +/// https://www.investopedia.com/articles/technical/060401.asp +/// https://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:weighted_moving_average +/// + public class Wma : AbstractBase { private readonly int _period; private readonly Convolution _convolution; + /// The number of data points used in the WMA calculation. + /// Thrown when period is less than 1. public Wma(int period) { if (period < 1) @@ -18,12 +47,19 @@ public class Wma : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of data points used in the WMA calculation. public Wma(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + /// + /// Generates the linearly weighted convolution kernel for the WMA calculation. + /// + /// The period for which to generate the kernel. + /// An array of normalized linearly decreasing weights for the convolution operation. private static double[] GenerateWmaKernel(int period) { double[] kernel = new double[period]; @@ -64,4 +100,4 @@ public class Wma : AbstractBase return result; } -} \ No newline at end of file +} diff --git a/lib/averages/Zlema.cs b/lib/averages/Zlema.cs index 1aae1e16..b88374b1 100644 --- a/lib/averages/Zlema.cs +++ b/lib/averages/Zlema.cs @@ -3,6 +3,31 @@ using System.Runtime.CompilerServices; namespace QuanTAlib { + /// + /// ZLEMA: Zero Lag Exponential Moving Average + /// A modified exponential moving average designed to reduce lag by incorporating + /// error correction based on predicted values. It estimates and removes lag by + /// extrapolating the trend using the difference between current and lagged prices. + /// + /// + /// The ZLEMA calculation process: + /// 1. Calculates lag period as (period - 1) / 2 + /// 2. Gets error correction term: 2 * price - lag_price + /// 3. Applies EMA to error-corrected price + /// 4. Results in reduced lag compared to standard EMA + /// + /// Key characteristics: + /// - Significantly reduced lag compared to EMA + /// - More responsive to price changes + /// - Uses error correction mechanism + /// - Maintains smoothness despite reduced lag + /// - Better trend following capabilities + /// + /// Sources: + /// John Ehlers and Ric Way - "Zero Lag (Well, Almost)" + /// Technical Analysis of Stocks and Commodities, 2010 + /// + public class Zlema : AbstractBase { private readonly CircularBuffer _buffer; @@ -10,6 +35,8 @@ namespace QuanTAlib private readonly Ema _ema; private double _lastZLEMA, _p_lastZLEMA; + /// The number of periods used in the ZLEMA calculation. + /// Thrown when period is less than 1. public Zlema(int period) { if (period < 1) @@ -24,6 +51,8 @@ namespace QuanTAlib Init(); } + /// The data source object that publishes updates. + /// The number of periods used in the ZLEMA calculation. public Zlema(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -59,8 +88,11 @@ namespace QuanTAlib _buffer.Add(Input.Value, Input.IsNew); + // Get lagged value and calculate error correction double lagValue = _buffer[Math.Max(0, _buffer.Count - 1 - _lag)]; double errorCorrection = 2 * Input.Value - lagValue; + + // Apply EMA to error-corrected value double zlema = _ema.Calc(new TValue(errorCorrection, Input.IsNew)).Value; _lastZLEMA = zlema; diff --git a/lib/errors/Huber.cs b/lib/errors/Huber.cs index c7510344..44d4915f 100644 --- a/lib/errors/Huber.cs +++ b/lib/errors/Huber.cs @@ -1,11 +1,44 @@ +using System; namespace QuanTAlib; +/// +/// Huber Loss: A robust error metric that combines squared error for small deviations +/// and absolute error for large deviations. This provides a balance between the high +/// sensitivity of MSE to outliers and the constant gradient of MAE. +/// +/// +/// The Huber Loss calculation process: +/// 1. For each point, calculates error between actual and predicted values +/// 2. If absolute error ≤ delta: uses squared error (like MSE) +/// 3. If absolute error > delta: uses linear error (like MAE) +/// 4. Averages the losses over the period +/// +/// Key characteristics: +/// - Combines benefits of MSE and MAE +/// - Less sensitive to outliers than MSE +/// - More sensitive to small errors than MAE +/// - Differentiable at all points +/// - Adjustable via delta parameter +/// +/// Formula: +/// For error e = actual - predicted: +/// L(e) = 0.5 * e² if |e| ≤ δ +/// L(e) = δ * (|e| - 0.5δ) if |e| > δ +/// +/// Sources: +/// Peter J. Huber - "Robust Estimation of a Location Parameter" +/// https://projecteuclid.org/euclid.aoms/1177703732 +/// + public class Huber : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; private readonly double _delta; + /// The number of points over which to calculate the loss. + /// The threshold between squared and linear loss (default 1.0). + /// Thrown when period is less than 1 or delta is not positive. public Huber(int period, double delta = 1.0) { if (period < 1) @@ -24,6 +57,9 @@ public class Huber : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of points over which to calculate the loss. + /// The threshold between squared and linear loss (default 1.0). public Huber(object source, int period, double delta = 1.0) : this(period, delta) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -53,6 +89,7 @@ public class Huber : AbstractBase double actual = Input.Value; _actualBuffer.Add(actual, Input.IsNew); + // If no predicted value provided, use mean of actual values double predicted = double.IsNaN(Input2.Value) ? _actualBuffer.Average() : Input2.Value; _predictedBuffer.Add(predicted, Input.IsNew); @@ -70,10 +107,12 @@ public class Huber : AbstractBase if (absError <= _delta) { + // Squared error for small deviations sumLoss += 0.5 * error * error; } else { + // Linear error for large deviations sumLoss += _delta * (absError - 0.5 * _delta); } } @@ -84,5 +123,4 @@ public class Huber : AbstractBase IsHot = _index >= WarmupPeriod; return huberloss; } - } diff --git a/lib/errors/Mae.cs b/lib/errors/Mae.cs index f043af52..3fc8efec 100644 --- a/lib/errors/Mae.cs +++ b/lib/errors/Mae.cs @@ -1,10 +1,40 @@ +using System; namespace QuanTAlib; +/// +/// MAE: Mean Absolute Error +/// A straightforward error metric that measures the average magnitude of errors +/// between predicted and actual values, without considering their direction. +/// MAE treats all individual differences equally in the average. +/// +/// +/// The MAE calculation process: +/// 1. Calculates absolute difference between each actual and predicted value +/// 2. Sums all absolute differences +/// 3. Divides by the number of observations +/// +/// Key characteristics: +/// - Linear scale (all differences weighted equally) +/// - Robust to outliers compared to MSE +/// - Easy to interpret (same units as data) +/// - Constant gradient for optimization +/// - Less sensitive to large errors than MSE +/// +/// Formula: +/// MAE = (1/n) * Σ|actual - predicted| +/// +/// Sources: +/// https://en.wikipedia.org/wiki/Mean_absolute_error +/// https://www.statisticshowto.com/absolute-error/ +/// + public class Mae : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; + /// The number of points over which to calculate the MAE. + /// Thrown when period is less than 1. public Mae(int period) { if (period < 1) @@ -18,6 +48,8 @@ public class Mae : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of points over which to calculate the MAE. public Mae(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -47,6 +79,7 @@ public class Mae : AbstractBase double actual = Input.Value; _actualBuffer.Add(actual, Input.IsNew); + // If no predicted value provided, use mean of actual values double predicted = double.IsNaN(Input2.Value) ? _actualBuffer.Average() : Input2.Value; _predictedBuffer.Add(predicted, Input.IsNew); @@ -68,5 +101,4 @@ public class Mae : AbstractBase IsHot = _index >= WarmupPeriod; return mae; } - } diff --git a/lib/errors/Mapd.cs b/lib/errors/Mapd.cs index 55a15d73..e36b4889 100644 --- a/lib/errors/Mapd.cs +++ b/lib/errors/Mapd.cs @@ -1,10 +1,42 @@ +using System; namespace QuanTAlib; +/// +/// MAPD: Mean Absolute Percentage Deviation +/// A percentage-based error metric that measures the average absolute percentage +/// difference between predicted and actual values. MAPD expresses accuracy as a +/// percentage, making it scale-independent and easy to interpret. +/// +/// +/// The MAPD calculation process: +/// 1. Calculates absolute percentage difference for each point +/// 2. Sums all absolute percentage differences +/// 3. Divides by the number of observations +/// +/// Key characteristics: +/// - Scale-independent (percentage-based) +/// - Easy to interpret (0-100% range) +/// - Useful for comparing different scales +/// - Cannot handle zero actual values +/// - Asymmetric (treats over/under predictions differently) +/// +/// Formula: +/// MAPD = (1/n) * Σ|((actual - predicted) / actual)| +/// +/// Sources: +/// https://en.wikipedia.org/wiki/Mean_absolute_percentage_error +/// https://www.statisticshowto.com/mean-absolute-percentage-error-mape/ +/// +/// Note: Also known as MAPE (Mean Absolute Percentage Error) in some contexts +/// + public class Mapd : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; + /// The number of points over which to calculate the MAPD. + /// Thrown when period is less than 1. public Mapd(int period) { if (period < 1) @@ -18,6 +50,8 @@ public class Mapd : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of points over which to calculate the MAPD. public Mapd(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -47,6 +81,7 @@ public class Mapd : AbstractBase double actual = Input.Value; _actualBuffer.Add(actual, Input.IsNew); + // If no predicted value provided, use mean of actual values double predicted = double.IsNaN(Input2.Value) ? _actualBuffer.Average() : Input2.Value; _predictedBuffer.Add(predicted, Input.IsNew); @@ -71,5 +106,4 @@ public class Mapd : AbstractBase IsHot = _index >= WarmupPeriod; return mapd; } - } diff --git a/lib/errors/Mape.cs b/lib/errors/Mape.cs index f19299f9..26d2492d 100644 --- a/lib/errors/Mape.cs +++ b/lib/errors/Mape.cs @@ -1,10 +1,42 @@ +using System; namespace QuanTAlib; +/// +/// MAPE: Mean Absolute Percentage Error +/// A percentage-based error metric that measures the average absolute percentage +/// difference between predicted and actual values. MAPE expresses accuracy as a +/// percentage, making it scale-independent and easy to interpret. +/// +/// +/// The MAPE calculation process: +/// 1. Calculates absolute percentage error for each point +/// 2. Sums all absolute percentage errors +/// 3. Divides by the number of observations +/// +/// Key characteristics: +/// - Scale-independent (percentage-based) +/// - Easy to interpret (0-100% range) +/// - Useful for comparing different scales +/// - Cannot handle zero actual values +/// - Asymmetric (treats over/under predictions differently) +/// +/// Formula: +/// MAPE = (1/n) * Σ|((actual - predicted) / actual)| * 100% +/// +/// Sources: +/// https://en.wikipedia.org/wiki/Mean_absolute_percentage_error +/// https://www.statisticshowto.com/mean-absolute-percentage-error-mape/ +/// +/// Note: Also known as MAPD (Mean Absolute Percentage Deviation) in some contexts +/// + public class Mape : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; + /// The number of points over which to calculate the MAPE. + /// Thrown when period is less than 1. public Mape(int period) { if (period < 1) @@ -18,6 +50,8 @@ public class Mape : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of points over which to calculate the MAPE. public Mape(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -47,6 +81,7 @@ public class Mape : AbstractBase double actual = Input.Value; _actualBuffer.Add(actual, Input.IsNew); + // If no predicted value provided, use mean of actual values double predicted = double.IsNaN(Input2.Value) ? _actualBuffer.Average() : Input2.Value; _predictedBuffer.Add(predicted, Input.IsNew); diff --git a/lib/errors/Mase.cs b/lib/errors/Mase.cs index 5b486ba7..073819ec 100644 --- a/lib/errors/Mase.cs +++ b/lib/errors/Mase.cs @@ -1,20 +1,41 @@ using System; - namespace QuanTAlib; /// -/// Represents the Mean Absolute Scaled Error (MASE) calculation. +/// MASE: Mean Absolute Scaled Error +/// A scale-free error metric that compares the mean absolute error of the forecast +/// with the mean absolute error of the naive forecast. MASE is particularly useful +/// for comparing forecast accuracy across different datasets. /// +/// +/// The MASE calculation process: +/// 1. Calculates mean absolute error of the forecast +/// 2. Calculates mean absolute error of naive forecast (using previous value) +/// 3. Divides forecast error by naive forecast error +/// +/// Key characteristics: +/// - Scale-free (independent of data scale) +/// - Handles zero values unlike percentage errors +/// - Symmetric (treats over/under predictions equally) +/// - Easy interpretation (MASE < 1 means better than naive forecast) +/// - Robust to outliers +/// +/// Formula: +/// MASE = MAE(forecast) / MAE(naive_forecast) +/// where naive_forecast[t] = actual[t-1] +/// +/// Sources: +/// Rob J. Hyndman - "Another Look at Forecast-Accuracy Metrics for Intermittent Demand" +/// https://robjhyndman.com/papers/another-look-at-measures-of-forecast-accuracy/ +/// + public class Mase : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; private readonly CircularBuffer _naiveBuffer; - /// - /// Initializes a new instance of the Mase class. - /// - /// The period for MASE calculation. + /// The number of points over which to calculate the MASE. /// Thrown when period is less than 1. public Mase(int period) { @@ -30,20 +51,14 @@ public class Mase : AbstractBase Init(); } - /// - /// Initializes a new instance of the Mase class with a source object. - /// - /// The source object for event subscription. - /// The period for MASE calculation. + /// The data source object that publishes updates. + /// The number of points over which to calculate the MASE. public Mase(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - /// - /// Initializes the Mase instance. - /// public override void Init() { base.Init(); @@ -52,10 +67,6 @@ public class Mase : AbstractBase _naiveBuffer.Clear(); } - /// - /// Manages the state of the Mase instance. - /// - /// Indicates if the input is new. protected override void ManageState(bool isNew) { if (isNew) @@ -65,10 +76,6 @@ public class Mase : AbstractBase } } - /// - /// Performs the MASE calculation. - /// - /// The calculated MASE value. protected override double Calculation() { ManageState(Input.IsNew); @@ -76,9 +83,11 @@ public class Mase : AbstractBase double actual = Input.Value; _actualBuffer.Add(actual, Input.IsNew); + // If no predicted value provided, use mean of actual values double predicted = double.IsNaN(Input2.Value) ? _actualBuffer.Average() : Input2.Value; _predictedBuffer.Add(predicted, Input.IsNew); + // Naive forecast uses previous actual value if (_actualBuffer.Count > 1) { _naiveBuffer.Add(_actualBuffer.GetSpan()[^2], Input.IsNew); @@ -90,6 +99,10 @@ public class Mase : AbstractBase return mase; } + /// + /// Calculates the MASE value by comparing forecast error to naive forecast error. + /// + /// The calculated MASE value, or positive infinity if naive error is zero. private double CalculateMase() { if (_actualBuffer.Count <= 1) return 0; @@ -104,6 +117,9 @@ public class Mase : AbstractBase return _naiveForecastError != 0 ? (sumAbsoluteError / _actualBuffer.Count) / _naiveForecastError : double.PositiveInfinity; } + /// + /// Calculates the sum of absolute errors between actual and predicted values. + /// private static double CalculateSumAbsoluteError(ReadOnlySpan actualValues, ReadOnlySpan predictedValues) { double sum = 0; @@ -114,6 +130,9 @@ public class Mase : AbstractBase return sum; } + /// + /// Calculates the naive forecast error using the previous value as prediction. + /// private static double CalculateNaiveForecastError(ReadOnlySpan actualValues, ReadOnlySpan naiveValues) { double sum = 0; diff --git a/lib/errors/Mda.cs b/lib/errors/Mda.cs index ba03bced..332337b8 100644 --- a/lib/errors/Mda.cs +++ b/lib/errors/Mda.cs @@ -1,10 +1,42 @@ +using System; namespace QuanTAlib; +/// +/// MDA: Mean Directional Accuracy +/// A metric that measures how well a forecast predicts the direction of change +/// rather than the magnitude. MDA focuses on whether the predicted movement +/// (up or down) matches the actual movement. +/// +/// +/// The MDA calculation process: +/// 1. For each consecutive pair of points: +/// - Calculate direction of actual change +/// - Calculate direction of predicted change +/// - Compare directions (match = 1, mismatch = 0) +/// 2. Average the directional matches +/// +/// Key characteristics: +/// - Scale-independent (only considers direction) +/// - Range is 0 to 1 (easy interpretation) +/// - Useful for trend prediction evaluation +/// - Ignores magnitude of changes +/// - Equal weight to all directional changes +/// +/// Formula: +/// MDA = (1/(n-1)) * Σ(sign(actual[t] - actual[t-1]) == sign(pred[t] - pred[t-1])) +/// +/// Sources: +/// https://www.sciencedirect.com/science/article/abs/pii/S0169207016000121 +/// "Evaluating Forecasting Performance" - International Journal of Forecasting +/// + public class Mda : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; + /// The number of points over which to calculate the MDA. + /// Thrown when period is less than 1. public Mda(int period) { if (period < 1) @@ -18,6 +50,8 @@ public class Mda : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of points over which to calculate the MDA. public Mda(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -47,6 +81,7 @@ public class Mda : AbstractBase double actual = Input.Value; _actualBuffer.Add(actual, Input.IsNew); + // If no predicted value provided, use mean of actual values double predicted = double.IsNaN(Input2.Value) ? _actualBuffer.Average() : Input2.Value; _predictedBuffer.Add(predicted, Input.IsNew); diff --git a/lib/errors/Me.cs b/lib/errors/Me.cs index a9118b39..ab75705a 100644 --- a/lib/errors/Me.cs +++ b/lib/errors/Me.cs @@ -1,10 +1,42 @@ +using System; namespace QuanTAlib; +/// +/// ME: Mean Error +/// A basic error metric that measures the average difference between actual and +/// predicted values. Unlike MAE, it allows positive and negative errors to cancel +/// out, making it useful for detecting systematic bias in predictions. +/// +/// +/// The ME calculation process: +/// 1. Calculates error (actual - predicted) for each point +/// 2. Sums all errors (allowing cancellation) +/// 3. Divides by the number of observations +/// +/// Key characteristics: +/// - Same units as input data +/// - Can detect systematic bias +/// - Positive ME indicates underprediction +/// - Negative ME indicates overprediction +/// - Errors can cancel out +/// +/// Formula: +/// ME = (1/n) * Σ(actual - predicted) +/// +/// Sources: +/// https://en.wikipedia.org/wiki/Mean_signed_difference +/// https://www.statisticshowto.com/mean-error/ +/// +/// Note: Also known as Mean Bias Error (MBE) or Mean Signed Difference (MSD) +/// + public class Me : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; + /// The number of points over which to calculate the ME. + /// Thrown when period is less than 1. public Me(int period) { if (period < 1) @@ -18,6 +50,8 @@ public class Me : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of points over which to calculate the ME. public Me(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -47,6 +81,7 @@ public class Me : AbstractBase double actual = Input.Value; _actualBuffer.Add(actual, Input.IsNew); + // If no predicted value provided, use mean of actual values double predicted = double.IsNaN(Input2.Value) ? _actualBuffer.Average() : Input2.Value; _predictedBuffer.Add(predicted, Input.IsNew); diff --git a/lib/errors/Mpe.cs b/lib/errors/Mpe.cs index c9fc3a88..cf18b88d 100644 --- a/lib/errors/Mpe.cs +++ b/lib/errors/Mpe.cs @@ -1,10 +1,43 @@ +using System; namespace QuanTAlib; +/// +/// MPE: Mean Percentage Error +/// A percentage-based error metric that measures the average percentage difference +/// between actual and predicted values. Like ME, it allows positive and negative +/// errors to cancel out, but expresses the bias in percentage terms. +/// +/// +/// The MPE calculation process: +/// 1. Calculates percentage error for each point +/// 2. Sums all percentage errors (allowing cancellation) +/// 3. Divides by the number of observations +/// +/// Key characteristics: +/// - Scale-independent (percentage-based) +/// - Can detect systematic bias +/// - Positive MPE indicates underprediction +/// - Negative MPE indicates overprediction +/// - Cannot handle zero actual values +/// - Errors can cancel out +/// +/// Formula: +/// MPE = (1/n) * Σ((actual - predicted) / actual) * 100% +/// +/// Sources: +/// https://en.wikipedia.org/wiki/Mean_percentage_error +/// https://www.statisticshowto.com/mean-percentage-error/ +/// +/// Note: Similar to MAPE but allows error cancellation +/// + public class Mpe : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; + /// The number of points over which to calculate the MPE. + /// Thrown when period is less than 1. public Mpe(int period) { if (period < 1) @@ -18,6 +51,8 @@ public class Mpe : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of points over which to calculate the MPE. public Mpe(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -47,6 +82,7 @@ public class Mpe : AbstractBase double actual = Input.Value; _actualBuffer.Add(actual, Input.IsNew); + // If no predicted value provided, use mean of actual values double predicted = double.IsNaN(Input2.Value) ? _actualBuffer.Average() : Input2.Value; _predictedBuffer.Add(predicted, Input.IsNew); diff --git a/lib/errors/Mse.cs b/lib/errors/Mse.cs index 7a3e33b2..2c8fa2f6 100644 --- a/lib/errors/Mse.cs +++ b/lib/errors/Mse.cs @@ -1,25 +1,42 @@ +using System; namespace QuanTAlib; /// -/// Represents a Mean Squared Error calculator that measures the average of the squares -/// of the differences between actual values and predicted values. +/// MSE: Mean Squared Error +/// A fundamental error metric that measures the average of squared differences +/// between predicted and actual values. MSE heavily penalizes large errors due +/// to the squaring operation. /// /// -/// The Mse class calculates the Mean Squared Error using a circular buffer -/// to efficiently manage the data points within the specified period. +/// The MSE calculation process: +/// 1. Calculates error (actual - predicted) for each point +/// 2. Squares each error value +/// 3. Averages the squared errors +/// +/// Key characteristics: +/// - Heavily penalizes large errors +/// - Always non-negative +/// - Units are squared (harder to interpret) +/// - More sensitive to outliers than MAE +/// - Differentiable (useful for optimization) +/// +/// Formula: +/// MSE = (1/n) * Σ(actual - predicted)² +/// +/// Sources: +/// https://en.wikipedia.org/wiki/Mean_squared_error +/// https://www.statisticshowto.com/probability-and-statistics/statistics-definitions/mean-squared-error/ +/// +/// Note: Often used in optimization due to its mathematical properties /// + public class Mse : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; - /// - /// Initializes a new instance of the Mse class with the specified period. - /// - /// The period over which to calculate the Mean Squared Error. - /// - /// Thrown when period is less than 1. - /// + /// The number of points over which to calculate the MSE. + /// Thrown when period is less than 1. public Mse(int period) { if (period < 1) @@ -33,20 +50,14 @@ public class Mse : AbstractBase Init(); } - /// - /// Initializes a new instance of the Mse class with the specified source and period. - /// - /// The source object to subscribe to for value updates. - /// The period over which to calculate the Mean Squared Error. + /// The data source object that publishes updates. + /// The number of points over which to calculate the MSE. public Mse(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - /// - /// Initializes the Mse instance by clearing the buffers. - /// public override void Init() { base.Init(); @@ -54,10 +65,6 @@ public class Mse : AbstractBase _predictedBuffer.Clear(); } - /// - /// Manages the state of the Mse instance based on whether new values are being processed. - /// - /// Indicates whether the current inputs are new values. protected override void ManageState(bool isNew) { if (isNew) @@ -67,17 +74,6 @@ public class Mse : AbstractBase } } - /// - /// Performs the Mean Squared Error calculation for the current period. - /// - /// - /// The calculated Mean Squared Error value for the current period. - /// - /// - /// This method calculates the Mean Squared Error using the formula: - /// MSE = sum((actual - predicted)^2) / n - /// where actual is each actual value, predicted is each predicted value, and n is the number of values. - /// protected override double Calculation() { ManageState(Input.IsNew); @@ -85,6 +81,7 @@ public class Mse : AbstractBase double actual = Input.Value; _actualBuffer.Add(actual, Input.IsNew); + // If no predicted value provided, use mean of actual values double predicted = double.IsNaN(Input2.Value) ? _actualBuffer.Average() : Input2.Value; _predictedBuffer.Add(predicted, Input.IsNew); diff --git a/lib/errors/Msle.cs b/lib/errors/Msle.cs index 464f4e10..27da073b 100644 --- a/lib/errors/Msle.cs +++ b/lib/errors/Msle.cs @@ -1,10 +1,43 @@ +using System; namespace QuanTAlib; +/// +/// MSLE: Mean Squared Logarithmic Error +/// A variation of MSE that operates on log-transformed values. MSLE is particularly +/// useful for data with exponential growth or when errors in larger values should +/// not be penalized more heavily than errors in smaller values. +/// +/// +/// The MSLE calculation process: +/// 1. Adds 1 to both actual and predicted values (to handle zeros) +/// 2. Takes natural log of both values +/// 3. Calculates squared difference of logs +/// 4. Averages the squared differences +/// +/// Key characteristics: +/// - Scale-independent due to log transformation +/// - Penalizes underestimates more than overestimates +/// - Handles exponential trends well +/// - More sensitive to relative differences +/// - Can handle zero values (adds 1 before log) +/// +/// Formula: +/// MSLE = (1/n) * Σ(log(actual + 1) - log(predicted + 1))² +/// +/// Sources: +/// https://scikit-learn.org/stable/modules/model_evaluation.html#mean-squared-logarithmic-error +/// https://medium.com/analytics-vidhya/root-mean-square-log-error-rmse-vs-rmlse-935c6cc1802a +/// +/// Note: Often used in cases where target values follow exponential growth +/// + public class Msle : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; + /// The number of points over which to calculate the MSLE. + /// Thrown when period is less than 1. public Msle(int period) { if (period < 1) @@ -18,6 +51,8 @@ public class Msle : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of points over which to calculate the MSLE. public Msle(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -47,6 +82,7 @@ public class Msle : AbstractBase double actual = Input.Value; _actualBuffer.Add(actual, Input.IsNew); + // If no predicted value provided, use mean of actual values double predicted = double.IsNaN(Input2.Value) ? _actualBuffer.Average() : Input2.Value; _predictedBuffer.Add(predicted, Input.IsNew); diff --git a/lib/errors/Rae.cs b/lib/errors/Rae.cs index 6b25a161..a9567a02 100644 --- a/lib/errors/Rae.cs +++ b/lib/errors/Rae.cs @@ -1,10 +1,42 @@ +using System; namespace QuanTAlib; +/// +/// RAE: Relative Absolute Error +/// A normalized error metric that compares the total absolute error to the total +/// magnitude of actual values. RAE provides a scale-independent measure of error +/// that is robust to the overall magnitude of the data. +/// +/// +/// The RAE calculation process: +/// 1. Calculates sum of absolute errors +/// 2. Calculates sum of absolute actual values +/// 3. Divides total error by total actual magnitude +/// +/// Key characteristics: +/// - Scale-independent (normalized by actual values) +/// - Range typically between 0 and 1 +/// - Easy to interpret (0 is perfect, 1 means error equals data magnitude) +/// - Robust to data scale changes +/// - Less sensitive to outliers than squared errors +/// +/// Formula: +/// RAE = Σ|actual - predicted| / Σ|actual| +/// +/// Sources: +/// https://en.wikipedia.org/wiki/Relative_absolute_error +/// https://www.sciencedirect.com/topics/engineering/relative-absolute-error +/// +/// Note: Values greater than 1 indicate predictions worse than using zero +/// + public class Rae : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; + /// The number of points over which to calculate the RAE. + /// Thrown when period is less than 1. public Rae(int period) { if (period < 1) @@ -18,6 +50,8 @@ public class Rae : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of points over which to calculate the RAE. public Rae(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -47,6 +81,7 @@ public class Rae : AbstractBase double actual = Input.Value; _actualBuffer.Add(actual, Input.IsNew); + // If no predicted value provided, use mean of actual values double predicted = double.IsNaN(Input2.Value) ? _actualBuffer.Average() : Input2.Value; _predictedBuffer.Add(predicted, Input.IsNew); diff --git a/lib/errors/Rmse.cs b/lib/errors/Rmse.cs index ba56b346..1474fd12 100644 --- a/lib/errors/Rmse.cs +++ b/lib/errors/Rmse.cs @@ -1,10 +1,43 @@ +using System; namespace QuanTAlib; +/// +/// RMSE: Root Mean Square Error +/// A widely used error metric that measures the square root of the average squared +/// differences between predicted and actual values. RMSE provides error measurements +/// in the same units as the original data. +/// +/// +/// The RMSE calculation process: +/// 1. Calculates error (actual - predicted) for each point +/// 2. Squares each error value +/// 3. Averages the squared errors +/// 4. Takes the square root of the average +/// +/// Key characteristics: +/// - Same units as input data (unlike MSE) +/// - Penalizes large errors more than small ones +/// - Always non-negative +/// - More interpretable than MSE +/// - Commonly used in regression problems +/// +/// Formula: +/// RMSE = √((1/n) * Σ(actual - predicted)²) +/// +/// Sources: +/// https://en.wikipedia.org/wiki/Root-mean-square_deviation +/// https://www.statisticshowto.com/probability-and-statistics/regression-analysis/rmse-root-mean-square-error/ +/// +/// Note: Square root of MSE, making it more interpretable in original units +/// + public class Rmse : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; + /// The number of points over which to calculate the RMSE. + /// Thrown when period is less than 1. public Rmse(int period) { if (period < 1) @@ -18,6 +51,8 @@ public class Rmse : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of points over which to calculate the RMSE. public Rmse(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -47,6 +82,7 @@ public class Rmse : AbstractBase double actual = Input.Value; _actualBuffer.Add(actual, Input.IsNew); + // If no predicted value provided, use mean of actual values double predicted = double.IsNaN(Input2.Value) ? _actualBuffer.Average() : Input2.Value; _predictedBuffer.Add(predicted, Input.IsNew); diff --git a/lib/errors/Rmsle.cs b/lib/errors/Rmsle.cs index cfeb4e35..846e8a65 100644 --- a/lib/errors/Rmsle.cs +++ b/lib/errors/Rmsle.cs @@ -1,10 +1,44 @@ +using System; namespace QuanTAlib; +/// +/// RMSLE: Root Mean Square Logarithmic Error +/// A variation of RMSE that operates on log-transformed values. RMSLE is particularly +/// useful for data with exponential growth or when relative errors in larger values +/// should be treated similarly to relative errors in smaller values. +/// +/// +/// The RMSLE calculation process: +/// 1. Adds 1 to both actual and predicted values (to handle zeros) +/// 2. Takes natural log of both values +/// 3. Calculates squared difference of logs +/// 4. Averages the squared differences +/// 5. Takes the square root +/// +/// Key characteristics: +/// - Scale-independent due to log transformation +/// - Penalizes underestimates more than overestimates +/// - Handles exponential trends well +/// - More sensitive to relative differences +/// - Can handle zero values (adds 1 before log) +/// +/// Formula: +/// RMSLE = √((1/n) * Σ(log(actual + 1) - log(predicted + 1))²) +/// +/// Sources: +/// https://www.kaggle.com/wiki/RootMeanSquaredLogarithmicError +/// https://medium.com/analytics-vidhya/root-mean-square-log-error-rmse-vs-rmlse-935c6cc1802a +/// +/// Note: Square root of MSLE, useful for data with exponential growth +/// + public class Rmsle : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; + /// The number of points over which to calculate the RMSLE. + /// Thrown when period is less than 1. public Rmsle(int period) { if (period < 1) @@ -18,6 +52,8 @@ public class Rmsle : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of points over which to calculate the RMSLE. public Rmsle(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -47,6 +83,7 @@ public class Rmsle : AbstractBase double actual = Input.Value; _actualBuffer.Add(actual, Input.IsNew); + // If no predicted value provided, use mean of actual values double predicted = double.IsNaN(Input2.Value) ? _actualBuffer.Average() : Input2.Value; _predictedBuffer.Add(predicted, Input.IsNew); diff --git a/lib/errors/Rse.cs b/lib/errors/Rse.cs index dc26bc95..0074cd1b 100644 --- a/lib/errors/Rse.cs +++ b/lib/errors/Rse.cs @@ -1,10 +1,43 @@ +using System; +using System.Linq; namespace QuanTAlib; +/// +/// RSE: Relative Squared Error +/// A normalized error metric that compares the squared error of predictions to +/// the variance of actual values. RSE provides a scale-independent measure of +/// prediction accuracy relative to the inherent variability in the data. +/// +/// +/// The RSE calculation process: +/// 1. Calculates sum of squared prediction errors +/// 2. Calculates sum of squared deviations from mean (variance) +/// 3. Divides squared error by variance and takes square root +/// +/// Key characteristics: +/// - Scale-independent (normalized by data variance) +/// - Range typically between 0 and 1 +/// - Easy interpretation relative to data variance +/// - Penalizes large errors more than small ones +/// - Accounts for data variability +/// +/// Formula: +/// RSE = √(Σ(actual - predicted)² / Σ(actual - mean(actual))²) +/// +/// Sources: +/// https://en.wikipedia.org/wiki/Relative_squared_error +/// https://www.sciencedirect.com/topics/engineering/relative-squared-error +/// +/// Note: Values less than 1 indicate predictions better than using mean +/// + public class Rse : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; + /// The number of points over which to calculate the RSE. + /// Thrown when period is less than 1. public Rse(int period) { if (period < 1) @@ -18,6 +51,8 @@ public class Rse : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of points over which to calculate the RSE. public Rse(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -47,6 +82,7 @@ public class Rse : AbstractBase double actual = Input.Value; _actualBuffer.Add(actual, Input.IsNew); + // If no predicted value provided, use mean of actual values double predicted = double.IsNaN(Input2.Value) ? _actualBuffer.Average() : Input2.Value; _predictedBuffer.Add(predicted, Input.IsNew); diff --git a/lib/errors/Rsquared.cs b/lib/errors/Rsquared.cs index e6213cb1..c7994def 100644 --- a/lib/errors/Rsquared.cs +++ b/lib/errors/Rsquared.cs @@ -1,10 +1,43 @@ +using System; +using System.Linq; namespace QuanTAlib; +/// +/// R-squared: Coefficient of Determination +/// A statistical measure that represents the proportion of variance in the dependent +/// variable that is predictable from the independent variable. R-squared provides +/// a measure of how well the predictions approximate the actual data. +/// +/// +/// The R-squared calculation process: +/// 1. Calculates total sum of squares (variance from mean) +/// 2. Calculates residual sum of squares (prediction errors) +/// 3. Computes 1 - (residual SS / total SS) +/// +/// Key characteristics: +/// - Range is typically 0 to 1 +/// - 1 indicates perfect prediction +/// - 0 indicates prediction no better than mean +/// - Scale-independent +/// - Widely used in regression analysis +/// +/// Formula: +/// R² = 1 - (Σ(actual - predicted)² / Σ(actual - mean(actual))²) +/// +/// Sources: +/// https://en.wikipedia.org/wiki/Coefficient_of_determination +/// https://www.statisticshowto.com/probability-and-statistics/coefficient-of-determination-r-squared/ +/// +/// Note: Can be negative if predictions are worse than using the mean +/// + public class Rsquared : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; + /// The number of points over which to calculate the R-squared value. + /// Thrown when period is less than 1. public Rsquared(int period) { if (period < 1) @@ -18,6 +51,8 @@ public class Rsquared : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of points over which to calculate the R-squared value. public Rsquared(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -47,6 +82,7 @@ public class Rsquared : AbstractBase double actual = Input.Value; _actualBuffer.Add(actual, Input.IsNew); + // If no predicted value provided, use mean of actual values double predicted = double.IsNaN(Input2.Value) ? _actualBuffer.Average() : Input2.Value; _predictedBuffer.Add(predicted, Input.IsNew); diff --git a/lib/errors/Smape.cs b/lib/errors/Smape.cs index 869ac820..73b47bb5 100644 --- a/lib/errors/Smape.cs +++ b/lib/errors/Smape.cs @@ -1,10 +1,42 @@ +using System; namespace QuanTAlib; +/// +/// SMAPE: Symmetric Mean Absolute Percentage Error +/// A variation of MAPE that treats positive and negative errors symmetrically. +/// SMAPE uses the average of actual and predicted values in the denominator, +/// making it more robust than MAPE for values close to zero. +/// +/// +/// The SMAPE calculation process: +/// 1. Calculates absolute difference between actual and predicted +/// 2. Divides by sum of absolute actual and predicted values +/// 3. Averages these ratios and multiplies by 200% +/// +/// Key characteristics: +/// - Symmetric treatment of errors +/// - Range is 0% to 200% +/// - More robust than MAPE near zero +/// - Scale-independent +/// - Handles both positive and negative values +/// +/// Formula: +/// SMAPE = (200/n) * Σ|actual - predicted| / (|actual| + |predicted|) +/// +/// Sources: +/// https://en.wikipedia.org/wiki/Symmetric_mean_absolute_percentage_error +/// https://www.sciencedirect.com/science/article/abs/pii/0169207085900059 +/// +/// Note: More stable than MAPE when actual values are close to zero +/// + public class Smape : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; + /// The number of points over which to calculate the SMAPE. + /// Thrown when period is less than 1. public Smape(int period) { if (period < 1) @@ -18,6 +50,8 @@ public class Smape : AbstractBase Init(); } + /// The data source object that publishes updates. + /// The number of points over which to calculate the SMAPE. public Smape(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -47,6 +81,7 @@ public class Smape : AbstractBase double actual = Input.Value; _actualBuffer.Add(actual, Input.IsNew); + // If no predicted value provided, use mean of actual values double predicted = double.IsNaN(Input2.Value) ? _actualBuffer.Average() : Input2.Value; _predictedBuffer.Add(predicted, Input.IsNew); diff --git a/lib/oscillators/Cmo.cs b/lib/oscillators/Cmo.cs index db0c545c..a303f67e 100644 --- a/lib/oscillators/Cmo.cs +++ b/lib/oscillators/Cmo.cs @@ -1,14 +1,47 @@ +using System; namespace QuanTAlib; /// -/// Represents a Chande Momentum Oscillator (CMO) calculator. +/// CMO: Chande Momentum Oscillator +/// A technical momentum indicator that measures the difference between upward and +/// downward momentum. CMO helps identify overbought and oversold conditions, as +/// well as trend strength and potential reversals. /// +/// +/// The CMO calculation process: +/// 1. Calculates price differences from previous period +/// 2. Separates positive (upward) and negative (downward) movements +/// 3. Sums upward and downward movements over period +/// 4. Calculates: 100 * ((sumUp - sumDown) / (sumUp + sumDown)) +/// +/// Key characteristics: +/// - Oscillates between -100 and +100 +/// - Values above +50 indicate overbought +/// - Values below -50 indicate oversold +/// - Zero line crossovers signal trend changes +/// - High absolute values suggest strong trends +/// +/// Formula: +/// CMO = 100 * ((ΣUp - ΣDown) / (ΣUp + ΣDown)) +/// where: +/// Up = positive price changes +/// Down = absolute negative price changes +/// +/// Sources: +/// Tushar Chande - "The New Technical Trader" (1994) +/// https://www.investopedia.com/terms/c/chandemomentumoscillator.asp +/// +/// Note: Similar to RSI but with different scaling and calculation method +/// + public class Cmo : AbstractBase { private readonly CircularBuffer _sumH; private readonly CircularBuffer _sumL; private double _prevValue, _p_prevValue; + /// The number of periods used in the CMO calculation. + /// Thrown when period is less than 1. public Cmo(int period) { if (period < 1) @@ -16,15 +49,12 @@ public class Cmo : AbstractBase _sumH = new(period); _sumL = new(period); - WarmupPeriod = period+1; + WarmupPeriod = period + 1; Name = $"CMO({period})"; } - /// - /// Initializes a new instance of the CMO class with a data source. - /// - /// The source object that publishes data. - /// The number of data points to consider. + /// The data source object that publishes updates. + /// The number of periods used in the CMO calculation. public Cmo(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -53,9 +83,11 @@ public class Cmo : AbstractBase _prevValue = Input.Value; } + // Calculate price difference double diff = Input.Value - _prevValue; _prevValue = Input.Value; + // Separate upward and downward movements if (diff > 0) { _sumH.Add(diff, Input.IsNew); @@ -67,11 +99,12 @@ public class Cmo : AbstractBase _sumL.Add(-diff, Input.IsNew); } - // Calculate sums for the specified period only + // Calculate sums for the specified period double sumH = _sumH.Sum(); double sumL = _sumL.Sum(); double divisor = sumH + sumL; + // Calculate CMO value return (Math.Abs(divisor) > double.Epsilon) ? 100.0 * ((sumH - sumL) / divisor) : 0.0; diff --git a/lib/oscillators/Rsi.cs b/lib/oscillators/Rsi.cs index 358e4d03..5f61fe27 100644 --- a/lib/oscillators/Rsi.cs +++ b/lib/oscillators/Rsi.cs @@ -1,16 +1,48 @@ using System; - namespace QuanTAlib; /// -/// Represents a Relative Strength Index (RSI) calculator following Wilder's algorithm. +/// RSI: Relative Strength Index +/// A momentum oscillator that measures the speed and magnitude of recent price +/// changes to evaluate overbought or oversold conditions. RSI compares the +/// magnitude of recent gains to recent losses. /// +/// +/// The RSI calculation process: +/// 1. Calculates price changes from previous period +/// 2. Separates gains and losses +/// 3. Calculates average gain and loss using Wilder's smoothing +/// 4. Computes relative strength (avg gain / avg loss) +/// 5. Normalizes to 0-100 scale: 100 - (100 / (1 + RS)) +/// +/// Key characteristics: +/// - Oscillates between 0 and 100 +/// - Traditional overbought level at 70 +/// - Traditional oversold level at 30 +/// - Centerline (50) crossovers signal trend changes +/// - Divergences suggest potential reversals +/// +/// Formula: +/// RSI = 100 - (100 / (1 + RS)) +/// where: +/// RS = Average Gain / Average Loss +/// Average Gain/Loss = Wilder's smoothed average over period +/// +/// Sources: +/// J. Welles Wilder Jr. - "New Concepts in Technical Trading Systems" (1978) +/// https://www.investopedia.com/terms/r/rsi.asp +/// +/// Note: Default period of 14 was recommended by Wilder +/// + public class Rsi : AbstractBase { private readonly Rma _avgGain; private readonly Rma _avgLoss; private double _prevValue, _p_prevValue; + /// The number of periods used in the RSI calculation (default 14). + /// Thrown when period is less than 1. public Rsi(int period = 14) { if (period < 1) @@ -22,11 +54,8 @@ public class Rsi : AbstractBase Name = $"RSI({period})"; } - /// - /// Initializes a new instance of the RSI class with a data source. - /// - /// The source object that publishes data. - /// The number of data points to consider. + /// The data source object that publishes updates. + /// The number of periods used in the RSI calculation. public Rsi(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -55,14 +84,17 @@ public class Rsi : AbstractBase _prevValue = Input.Value; } + // Calculate price change and separate gains/losses double change = Input.Value - _prevValue; double gain = Math.Max(change, 0); double loss = Math.Max(-change, 0); _prevValue = Input.Value; + // Calculate smoothed averages using Wilder's method _avgGain.Calc(gain, IsNew: Input.IsNew); _avgLoss.Calc(loss, IsNew: Input.IsNew); + // Calculate RSI double rsi = (_avgLoss.Value > 0) ? 100 - (100 / (1 + (_avgGain.Value / _avgLoss.Value))) : 100; return rsi; diff --git a/lib/oscillators/Rsx.cs b/lib/oscillators/Rsx.cs index b2d365c6..6401c244 100644 --- a/lib/oscillators/Rsx.cs +++ b/lib/oscillators/Rsx.cs @@ -1,10 +1,39 @@ using System; - namespace QuanTAlib; /// -/// Jurik's superior replacement for RSI +/// RSX: Relative Strength eXtended +/// An enhanced version of RSI developed by Mark Jurik that applies JMA (Jurik Moving +/// Average) smoothing to the RSI calculation. RSX provides smoother signals with +/// less noise while maintaining responsiveness to significant price movements. /// +/// +/// The RSX calculation process: +/// 1. Calculates traditional RSI values +/// 2. Applies JMA smoothing to RSI output +/// 3. Uses optimized parameters for noise reduction +/// 4. Maintains RSI's 0-100 scale +/// +/// Key characteristics: +/// - Smoother than traditional RSI +/// - Better noise reduction +/// - Maintains responsiveness to significant moves +/// - Same interpretation as RSI (0-100 scale) +/// - Fewer false signals than RSI +/// +/// Formula: +/// RSX = JMA(RSI(price)) +/// where: +/// RSI = standard Relative Strength Index +/// JMA = Jurik Moving Average with optimized parameters +/// +/// Sources: +/// Mark Jurik - "The Jurik RSX" +/// https://www.jurikresearch.com/ +/// +/// Note: Proprietary enhancement of RSI using JMA technology +/// + public class Rsx : AbstractBase { private readonly Rma _avgGain; @@ -12,6 +41,10 @@ public class Rsx : AbstractBase private readonly Jma _rsx; private double _prevValue, _p_prevValue; + /// The number of periods for RSI calculation (default 14). + /// The phase parameter for JMA smoothing (default 0). + /// The factor parameter for smoothing control (default 0.55). + /// Thrown when period is less than 1. public Rsx(int period = 14, int phase = 0, double factor = 0.55) { if (period < 1) @@ -24,13 +57,10 @@ public class Rsx : AbstractBase Name = $"RSX({period})"; } - /// - /// Initializes a new instance of the RSX class with a data source. - /// - /// The source object that publishes data. - /// The number of data points to consider. - /// The phase parameter. - /// The factor parameter. + /// The data source object that publishes updates. + /// The number of periods for RSI calculation. + /// The phase parameter for JMA smoothing. + /// The factor parameter for smoothing control. public Rsx(object source, int period, int phase = 0, double factor = 0.55) : this(period, phase, factor) { var pubEvent = source.GetType().GetEvent("Pub"); @@ -59,15 +89,18 @@ public class Rsx : AbstractBase _prevValue = Input.Value; } + // Calculate RSI components double change = Input.Value - _prevValue; double gain = Math.Max(change, 0); double loss = Math.Max(-change, 0); _prevValue = Input.Value; + // Calculate RSI _avgGain.Calc(gain, IsNew: Input.IsNew); _avgLoss.Calc(loss, IsNew: Input.IsNew); - double rsi = (_avgLoss.Value > 0) ? 100 - (100 / (1 + (_avgGain.Value / _avgLoss.Value))) : 100; + + // Apply JMA smoothing double rsx = _rsx.Calc(rsi, Input.IsNew); return rsx; diff --git a/lib/statistics/Curvature.cs b/lib/statistics/Curvature.cs index 59330724..2d51c8d3 100644 --- a/lib/statistics/Curvature.cs +++ b/lib/statistics/Curvature.cs @@ -1,15 +1,41 @@ +using System; +using System.Linq; namespace QuanTAlib; /// -/// Calculates the rate of change of the slope over a specified period. -/// Provides insights into trend acceleration or deceleration. +/// Curvature: Second Derivative Rate of Change +/// A statistical measure that calculates the rate of change of the slope over time. +/// Curvature provides insights into trend acceleration or deceleration by measuring +/// how quickly the slope (first derivative) is changing. /// /// -/// Curvature is a second-order derivative that measures how quickly the slope (first-order derivative) is changing. -/// Positive curvature indicates accelerating uptrends or decelerating downtrends. -/// Negative curvature indicates decelerating uptrends or accelerating downtrends. -/// This indicator can be useful for identifying potential trend reversals or confirming trend strength. +/// The Curvature calculation process: +/// 1. Calculates slope values over the specified period +/// 2. Applies least squares regression to slope values +/// 3. Provides slope of slopes (curvature) +/// 4. Includes additional statistical measures (R², StdDev) +/// +/// Key characteristics: +/// - Measures trend acceleration/deceleration +/// - Positive values indicate accelerating uptrends or decelerating downtrends +/// - Negative values indicate decelerating uptrends or accelerating downtrends +/// - Helps identify potential trend reversals +/// - Provides trend momentum information +/// +/// Formula: +/// Curvature = Σ((x - x̄)(y - ȳ)) / Σ((x - x̄)²) +/// where: +/// x = time points +/// y = slope values +/// x̄, ȳ = respective means +/// +/// Sources: +/// https://en.wikipedia.org/wiki/Curvature +/// https://www.sciencedirect.com/topics/mathematics/curve-fitting +/// +/// Note: Second-order derivative providing acceleration insights /// + public class Curvature : AbstractBase { private readonly int _period; @@ -36,13 +62,8 @@ public class Curvature : AbstractBase ///
public double? Line { get; private set; } - /// - /// Initializes a new instance of the Curvature class. - /// - /// The number of data points to consider for calculation. - /// - /// Thrown when the period is 2 or less. - /// + /// The number of points to consider for calculation. + /// Thrown when period is 2 or less. public Curvature(int period) { if (period <= 2) @@ -59,20 +80,14 @@ public class Curvature : AbstractBase Init(); } - /// - /// Initializes a new instance of the Curvature class with a data source. - /// - /// The source object that publishes data. - /// The number of data points to consider. + /// The data source object that publishes updates. + /// The number of points to consider for calculation. public Curvature(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - /// - /// Resets the Curvature indicator to its initial state. - /// public override void Init() { base.Init(); @@ -83,10 +98,6 @@ public class Curvature : AbstractBase Line = null; } - /// - /// Manages the state of the indicator. - /// - /// Indicates if the current data point is new. protected override void ManageState(bool isNew) { if (isNew) @@ -96,16 +107,6 @@ public class Curvature : AbstractBase } } - /// - /// Performs the curvature calculation. - /// - /// - /// The calculated curvature value. Positive for increasing slope, negative for decreasing. - /// - /// - /// Uses least squares method for optimal calculation. Also computes additional statistics - /// such as Intercept, Standard Deviation, R-Squared, and Line value. - /// protected override double Calculation() { ManageState(Input.IsNew); diff --git a/lib/statistics/Entropy.cs b/lib/statistics/Entropy.cs index 7803f7e4..f388b183 100644 --- a/lib/statistics/Entropy.cs +++ b/lib/statistics/Entropy.cs @@ -1,33 +1,53 @@ +using System; +using System.Linq; namespace QuanTAlib; /// -/// Measures the unpredictability of data using Shannon's Entropy. -/// Provides insights into the randomness or information content of the time series. +/// Entropy: Information Content Measure +/// A statistical measure that quantifies the unpredictability or randomness in +/// a time series using Shannon's Entropy. Higher entropy indicates more randomness +/// and uncertainty in the data. /// /// -/// Shannon's Entropy quantifies the average amount of information contained in a message. -/// In the context of time series analysis, it can be used to: -/// - Detect regime changes or structural breaks in the data. -/// - Assess the complexity or predictability of price movements. -/// - Identify periods of high uncertainty or information flow in the market. -/// The entropy value is normalized between 0 and 1, where 1 indicates maximum randomness -/// and 0 indicates perfect predictability. +/// The Entropy calculation process: +/// 1. Groups values to calculate probabilities +/// 2. Applies Shannon's entropy formula +/// 3. Normalizes result to 0-1 range +/// 4. Adjusts for number of unique values +/// +/// Key characteristics: +/// - Range from 0 (predictable) to 1 (random) +/// - Measures information content +/// - Detects regime changes +/// - Identifies market uncertainty +/// - Scale-independent measure +/// +/// Formula: +/// H = -Σ(p(x) * log₂(p(x))) / log₂(n) +/// where: +/// p(x) = probability of value x +/// n = number of unique values +/// +/// Applications: +/// - Detect market regime changes +/// - Assess price movement predictability +/// - Identify periods of high uncertainty +/// - Measure information flow in markets +/// +/// Sources: +/// Claude Shannon - "A Mathematical Theory of Communication" (1948) +/// https://en.wikipedia.org/wiki/Entropy_(information_theory) +/// +/// Note: Normalized to [0,1] for easier interpretation /// + public class Entropy : AbstractBase { - /// - /// The number of data points to consider for the entropy calculation. - /// private readonly int Period; private readonly CircularBuffer _buffer; - /// - /// Initializes a new instance of the Entropy class. - /// - /// The number of data points to consider for calculation. - /// - /// Thrown when the period is less than 2. - /// + /// The number of points to consider for entropy calculation. + /// Thrown when period is less than 2. public Entropy(int period) { if (period < 2) @@ -42,30 +62,20 @@ public class Entropy : AbstractBase Init(); } - /// - /// Initializes a new instance of the Entropy class with a data source. - /// - /// The source object that publishes data. - /// The number of data points to consider. + /// The data source object that publishes updates. + /// The number of points to consider for entropy calculation. public Entropy(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - /// - /// Resets the Entropy indicator to its initial state. - /// public override void Init() { base.Init(); _buffer.Clear(); } - /// - /// Manages the state of the indicator. - /// - /// Indicates if the current data point is new. protected override void ManageState(bool isNew) { if (isNew) @@ -75,17 +85,6 @@ public class Entropy : AbstractBase } } - /// - /// Performs the entropy calculation. - /// - /// - /// The calculated entropy value, normalized between 0 and 1. - /// 1 indicates maximum randomness, 0 indicates perfect predictability. - /// - /// - /// Uses Shannon's Entropy formula and normalizes the result based on the - /// number of unique values in the current period. - /// protected override double Calculation() { ManageState(Input.IsNew); @@ -93,22 +92,22 @@ public class Entropy : AbstractBase _buffer.Add(Input.Value, Input.IsNew); double entropy = 0; - if (_index > 1) // We need at least two data points for entropy calculation + if (_index > 1) // Need at least two data points for entropy calculation { var values = _buffer.GetSpan().ToArray(); int n = values.Length; - // Calculate probabilities + // Calculate probabilities for each unique value var groupedValues = values.GroupBy(x => x).Select(g => new { Value = g.Key, Count = g.Count() }); - // Use the actual count of values for probability calculation + // Calculate Shannon's entropy foreach (var group in groupedValues) { double probability = (double)group.Count / n; entropy -= probability * Math.Log2(probability); } - // Normalize the entropy based on the current number of unique values + // Normalize by maximum possible entropy for current unique values int uniqueValueCount = groupedValues.Count(); double maxEntropy = Math.Log2(uniqueValueCount); @@ -116,7 +115,7 @@ public class Entropy : AbstractBase } else { - entropy = 1; // Default to maximum entropy when insufficient data + entropy = 1; // Maximum entropy when insufficient data } IsHot = _buffer.Count >= Period; diff --git a/lib/statistics/Kurtosis.cs b/lib/statistics/Kurtosis.cs index cd32c92f..0166b65b 100644 --- a/lib/statistics/Kurtosis.cs +++ b/lib/statistics/Kurtosis.cs @@ -1,38 +1,54 @@ +using System; +using System.Linq; namespace QuanTAlib; /// -/// Calculates excess kurtosis using the Sheskin Algorithm. -/// Measures the "tailedness" of the probability distribution of a real-valued random variable. +/// Kurtosis: Distribution Tail Weight Measure +/// A statistical measure that quantifies the "tailedness" of a distribution using +/// the Sheskin Algorithm. Kurtosis indicates whether data has heavy tails (more +/// outliers) or light tails (fewer outliers) compared to a normal distribution. /// /// -/// Kurtosis is a measure of the combined weight of a distribution's tails relative to the center of the distribution. -/// In financial time series analysis, kurtosis can provide insights into: -/// - The frequency and magnitude of extreme returns. -/// - The potential for outliers or "black swan" events. -/// - The shape of the return distribution compared to a normal distribution. +/// The Kurtosis calculation process: +/// 1. Calculates mean of the data +/// 2. Computes squared and fourth power deviations +/// 3. Applies Sheskin Algorithm for excess kurtosis +/// 4. Adjusts for sample size bias /// -/// Interpretation: -/// - Excess kurtosis > 0: Heavy-tailed distribution (more extreme values than a normal distribution) -/// - Excess kurtosis = 0: Normal distribution -/// - Excess kurtosis < 0: Light-tailed distribution (fewer extreme values than a normal distribution) +/// Key characteristics: +/// - Measures tail weight relative to normal distribution +/// - Positive values indicate heavy tails +/// - Negative values indicate light tails +/// - Zero indicates normal distribution +/// - Sensitive to extreme values /// -/// High kurtosis in financial returns may indicate a higher risk of extreme events. +/// Formula: +/// K = [n(n+1)Σ(x-μ)⁴] / [s⁴(n-1)(n-2)(n-3)] - [3(n-1)²]/[(n-2)(n-3)] +/// where: +/// n = sample size +/// μ = mean +/// s = standard deviation +/// +/// Market Applications: +/// - Identify potential for extreme moves +/// - Assess risk of "black swan" events +/// - Compare return distributions +/// - Risk management tool +/// +/// Sources: +/// David J. Sheskin - "Handbook of Parametric and Nonparametric Statistical Procedures" +/// https://en.wikipedia.org/wiki/Kurtosis +/// +/// Note: Returns excess kurtosis (normal distribution = 0) /// + public class Kurtosis : AbstractBase { - /// - /// The number of data points to consider for the kurtosis calculation. - /// private readonly int Period; private readonly CircularBuffer _buffer; - /// - /// Initializes a new instance of the Kurtosis class. - /// - /// The number of data points to consider for calculation. - /// - /// Thrown when the period is less than 4. - /// + /// The number of points to consider for kurtosis calculation. + /// Thrown when period is less than 4. public Kurtosis(int period) { if (period < 4) @@ -47,30 +63,20 @@ public class Kurtosis : AbstractBase Init(); } - /// - /// Initializes a new instance of the Kurtosis class with a data source. - /// - /// The source object that publishes data. - /// The number of data points to consider. + /// The data source object that publishes updates. + /// The number of points to consider for kurtosis calculation. public Kurtosis(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - /// - /// Resets the Kurtosis indicator to its initial state. - /// public override void Init() { base.Init(); _buffer.Clear(); } - /// - /// Manages the state of the indicator. - /// - /// Indicates if the current data point is new. protected override void ManageState(bool isNew) { if (isNew) @@ -80,22 +86,6 @@ public class Kurtosis : AbstractBase } } - /// - /// Performs the kurtosis calculation. - /// - /// - /// The calculated excess kurtosis. Positive for heavy-tailed distributions, - /// negative for light-tailed distributions. - /// - /// - /// Uses the Sheskin Algorithm for kurtosis calculation. - /// Requires at least 4 data points for a valid calculation. - /// - /// Interpretation of results: - /// - Positive values indicate a distribution with heavier tails and a higher peak compared to a normal distribution. - /// - Negative values indicate a distribution with lighter tails and a lower peak compared to a normal distribution. - /// - A value close to 0 suggests a distribution similar to a normal distribution in terms of tailedness. - /// protected override double Calculation() { ManageState(Input.IsNew); @@ -103,14 +93,15 @@ public class Kurtosis : AbstractBase _buffer.Add(Input.Value, Input.IsNew); double kurtosis = 0; - if (_buffer.Count > 3) + if (_buffer.Count > 3) // Need at least 4 points for valid calculation { var values = _buffer.GetSpan().ToArray(); double mean = values.Average(); double n = values.Length; - double s2 = 0; - double s4 = 0; + // Calculate squared and fourth power deviations + double s2 = 0; // Sum of squared deviations + double s4 = 0; // Sum of fourth power deviations for (int i = 0; i < values.Length; i++) { @@ -121,7 +112,7 @@ public class Kurtosis : AbstractBase double variance = s2 / (n - 1); - // Sheskin Algorithm + // Sheskin Algorithm for excess kurtosis kurtosis = (n * (n + 1) * s4) / (variance * variance * (n - 3) * (n - 1) * (n - 2)) - (3 * (n - 1) * (n - 1) / ((n - 2) * (n - 3))); } diff --git a/lib/statistics/Max.cs b/lib/statistics/Max.cs index 2f110a03..13ae8ad1 100644 --- a/lib/statistics/Max.cs +++ b/lib/statistics/Max.cs @@ -1,63 +1,58 @@ +using System; namespace QuanTAlib; /// -/// Calculates the maximum value over a specified period, with an optional decay factor. -/// Useful for tracking the highest point in a time series with the ability to gradually forget old peaks. +/// MAX: Maximum Value with Decay +/// A statistical measure that tracks the highest value over a specified period, +/// with an optional decay factor to gradually reduce the influence of older peaks. +/// This adaptive approach allows the indicator to respond to changing market conditions. /// /// -/// The Max indicator is particularly useful in financial analysis for: -/// - Identifying resistance levels in price charts. -/// - Tracking the highest price over a given period. -/// - Implementing trailing stop-loss strategies. +/// The MAX calculation process: +/// 1. Tracks highest value in current period +/// 2. Applies exponential decay to old peaks +/// 3. Adjusts decay based on time since last peak +/// 4. Caps result at current period's maximum /// -/// The decay factor allows the indicator to adapt to changing market conditions by -/// gradually reducing the influence of older maximum values. +/// Key characteristics: +/// - Tracks absolute highest values +/// - Optional decay for adaptivity +/// - Maintains historical context +/// - Smooth transitions with decay +/// - Period-based windowing +/// +/// Formula: +/// decay = 1 - e^(-halfLife * timeSinceMax / period) +/// max = max - decay * (max - periodAverage) +/// max = min(max, periodMaximum) +/// +/// Market Applications: +/// - Identify resistance levels +/// - Track price peaks +/// - Implement trailing stops +/// - Monitor price extremes +/// - Adaptive trend following +/// +/// Sources: +/// Technical Analysis of Financial Markets +/// https://www.investopedia.com/terms/r/resistance.asp +/// +/// Note: Decay factor allows for adaptive peak tracking /// + public class Max : AbstractBase { - /// - /// The number of data points to consider for the maximum calculation. - /// private readonly int Period; - - /// - /// Circular buffer to store the most recent data points. - /// private readonly CircularBuffer _buffer; - - /// - /// The half-life decay factor used to gradually forget old peaks. - /// private readonly double _halfLife; - - /// - /// The current maximum value. - /// private double _currentMax; - - /// - /// The previous maximum value. - /// private double _p_currentMax; - - /// - /// The number of periods since a new maximum was set. - /// private int _timeSinceNewMax; - - /// - /// The previous value of _timeSinceNewMax. - /// private int _p_timeSinceNewMax; - /// - /// Initializes a new instance of the Max class. - /// - /// The number of data points to consider. Must be at least 1. - /// Half-life decay factor. Set to 0 for no decay, higher for faster forgetting of old peaks. Default is 0. - /// - /// Thrown when the period is less than 1 or decay is negative. - /// + /// The number of points to consider for maximum calculation. + /// Half-life decay factor (0 for no decay, higher for faster forgetting). + /// Thrown when period is less than 1 or decay is negative. public Max(int period, double decay = 0) { if (period < 1) @@ -78,21 +73,15 @@ public class Max : AbstractBase Init(); } - /// - /// Initializes a new instance of the Max class with a data source. - /// - /// The source object that publishes data. - /// The number of data points to consider. - /// Half-life decay factor. Default is 0. + /// The data source object that publishes updates. + /// The number of points to consider for maximum calculation. + /// Half-life decay factor (default 0). public Max(object source, int period, double decay = 0) : this(period, decay) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - /// - /// Resets the Max indicator to its initial state. - /// public override void Init() { base.Init(); @@ -100,10 +89,6 @@ public class Max : AbstractBase _timeSinceNewMax = 0; } - /// - /// Manages the state of the indicator. - /// - /// Indicates if the current data point is new. protected override void ManageState(bool isNew) { if (isNew) @@ -121,29 +106,23 @@ public class Max : AbstractBase } } - /// - /// Performs the max calculation. - /// - /// - /// The current maximum value, potentially adjusted by the decay factor. - /// - /// - /// Uses a decay factor to gradually forget old peaks. The max value is always - /// capped by the highest value in the current period. - /// protected override double Calculation() { ManageState(Input.IsNew); _buffer.Add(Input.Value, Input.IsNew); + // Update maximum if new value is higher if (Input.Value >= _currentMax) { _currentMax = Input.Value; _timeSinceNewMax = 0; } + // Apply decay based on time since last maximum double decayRate = 1 - Math.Exp(-_halfLife * _timeSinceNewMax / Period); _currentMax -= decayRate * (_currentMax - _buffer.Average()); + + // Ensure maximum doesn't exceed current period's highest value _currentMax = Math.Min(_currentMax, _buffer.Max()); IsHot = true; diff --git a/lib/statistics/Median.cs b/lib/statistics/Median.cs index c999099e..fc203704 100644 --- a/lib/statistics/Median.cs +++ b/lib/statistics/Median.cs @@ -1,33 +1,52 @@ +using System; +using System.Linq; namespace QuanTAlib; /// -/// Calculates the median value over a specified period. -/// Provides a measure of central tendency that is robust to outliers. +/// Median: Central Tendency Measure +/// A robust statistical measure that finds the middle value in a sorted dataset. +/// The median is less sensitive to outliers than the mean, making it particularly +/// useful for analyzing price data with extreme values. /// /// -/// The Median indicator is particularly useful in financial analysis for: -/// - Providing a robust measure of central tendency that is less affected by extreme values than the mean. -/// - Identifying the middle value in a dataset, which can be helpful in understanding price distributions. -/// - Serving as a basis for other indicators or trading strategies that require a stable reference point. +/// The Median calculation process: +/// 1. Collects values over specified period +/// 2. Sorts values in ascending order +/// 3. Finds middle value(s) +/// 4. Averages two middle values if even count /// -/// Unlike the mean, the median is not influenced by extreme outliers, making it valuable -/// in markets with occasional large price swings or in the presence of data anomalies. +/// Key characteristics: +/// - Robust to outliers +/// - Always represents actual data point +/// - Splits dataset in half +/// - More stable than mean +/// - Maintains data scale +/// +/// Formula: +/// For odd n: median = value at position (n+1)/2 +/// For even n: median = (value at n/2 + value at (n/2)+1) / 2 +/// +/// Market Applications: +/// - Price distribution analysis +/// - Trend identification +/// - Outlier detection +/// - Support/resistance levels +/// - Filter extreme movements +/// +/// Sources: +/// https://en.wikipedia.org/wiki/Median +/// "Statistics for Trading" - Technical Analysis of Financial Markets +/// +/// Note: More robust than mean for non-normal distributions /// + public class Median : AbstractBase { - /// - /// The number of data points to consider for the median calculation. - /// private readonly int Period; private readonly CircularBuffer _buffer; - /// - /// Initializes a new instance of the Median class. - /// - /// The number of data points to consider. Must be at least 1. - /// - /// Thrown when the period is less than 1. - /// + /// The number of points to consider for median calculation. + /// Thrown when period is less than 1. public Median(int period) { if (period < 1) @@ -42,30 +61,20 @@ public class Median : AbstractBase Init(); } - /// - /// Initializes a new instance of the Median class with a data source. - /// - /// The source object that publishes data. - /// The number of data points to consider. + /// The data source object that publishes updates. + /// The number of points to consider for median calculation. public Median(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - /// - /// Resets the Median indicator to its initial state. - /// public override void Init() { base.Init(); _buffer.Clear(); } - /// - /// Manages the state of the indicator. - /// - /// Indicates if the current data point is new. protected override void ManageState(bool isNew) { if (isNew) @@ -75,16 +84,6 @@ public class Median : AbstractBase } } - /// - /// Performs the median calculation. - /// - /// - /// The current median value of the dataset. - /// - /// - /// Uses a sorting approach to find the median. If there's not enough data, - /// it uses the average as a temporary measure. - /// protected override double Calculation() { ManageState(Input.IsNew); @@ -93,11 +92,15 @@ public class Median : AbstractBase double median; if (_index >= Period) { + // Get sorted copy of values var sortedValues = _buffer.GetSpan().ToArray(); Array.Sort(sortedValues); int middleIndex = sortedValues.Length / 2; - median = (sortedValues.Length % 2 == 0) ? (sortedValues[middleIndex - 1] + sortedValues[middleIndex]) / 2.0 : sortedValues[middleIndex]; + // Calculate median based on odd/even count + median = (sortedValues.Length % 2 == 0) + ? (sortedValues[middleIndex - 1] + sortedValues[middleIndex]) / 2.0 + : sortedValues[middleIndex]; } else { diff --git a/lib/statistics/Min.cs b/lib/statistics/Min.cs index 5832bdc4..2ebede8c 100644 --- a/lib/statistics/Min.cs +++ b/lib/statistics/Min.cs @@ -1,63 +1,58 @@ +using System; namespace QuanTAlib; /// -/// Represents a minimum value calculator with optional decay over a specified period. -/// This class calculates the minimum value within a given period, with the ability to -/// apply a decay factor to give more weight to recent values. +/// MIN: Minimum Value with Decay +/// A statistical measure that tracks the lowest value over a specified period, +/// with an optional decay factor to gradually reduce the influence of older lows. +/// This adaptive approach allows the indicator to respond to changing market conditions. /// /// -/// The Min class uses a circular buffer to store values and calculates the minimum -/// efficiently. It also implements a decay mechanism to adjust the minimum value over -/// time, allowing for a more responsive indicator in changing market conditions. +/// The MIN calculation process: +/// 1. Tracks lowest value in current period +/// 2. Applies exponential decay to old lows +/// 3. Adjusts decay based on time since last low +/// 4. Caps result at current period's minimum /// -/// The decay factor allows the indicator to "forget" old minimum values gradually, -/// which can be useful in adapting to new price trends or market regimes. +/// Key characteristics: +/// - Tracks absolute lowest values +/// - Optional decay for adaptivity +/// - Maintains historical context +/// - Smooth transitions with decay +/// - Period-based windowing +/// +/// Formula: +/// decay = 1 - e^(-halfLife * timeSinceMin / period) +/// min = min + decay * (periodAverage - min) +/// min = max(min, periodMinimum) +/// +/// Market Applications: +/// - Identify support levels +/// - Track price troughs +/// - Implement trailing stops +/// - Monitor price extremes +/// - Adaptive trend following +/// +/// Sources: +/// Technical Analysis of Financial Markets +/// https://www.investopedia.com/terms/s/support.asp +/// +/// Note: Decay factor allows for adaptive low tracking /// + public class Min : AbstractBase { - /// - /// The number of data points to consider for the minimum calculation. - /// private readonly int Period; - - /// - /// Circular buffer to store the most recent data points. - /// private readonly CircularBuffer _buffer; - - /// - /// The half-life decay factor used to gradually forget old minimums. - /// private readonly double _halfLife; - - /// - /// The current minimum value. - /// private double _currentMin; - - /// - /// The previous minimum value. - /// private double _p_currentMin; - - /// - /// The number of periods since a new minimum was set. - /// private int _timeSinceNewMin; - - /// - /// The previous value of _timeSinceNewMin. - /// private int _p_timeSinceNewMin; - /// - /// Initializes a new instance of the Min class with the specified period and decay. - /// - /// The period over which to calculate the minimum value. - /// The decay factor to apply to older values. Higher values cause faster forgetting of old minimums. Default is 0 (no decay). - /// - /// Thrown when period is less than 1 or decay is negative. - /// + /// The number of points to consider for minimum calculation. + /// Half-life decay factor (0 for no decay, higher for faster forgetting). + /// Thrown when period is less than 1 or decay is negative. public Min(int period, double decay = 0) { if (period < 1) @@ -76,21 +71,15 @@ public class Min : AbstractBase Init(); } - /// - /// Initializes a new instance of the Min class with the specified source, period, and decay. - /// - /// The source object to subscribe to for value updates. - /// The period over which to calculate the minimum value. - /// The decay factor to apply to older values. Higher values cause faster forgetting of old minimums. Default is 0 (no decay). + /// The data source object that publishes updates. + /// The number of points to consider for minimum calculation. + /// Half-life decay factor (default 0). public Min(object source, int period, double decay = 0) : this(period, decay) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - /// - /// Initializes the Min instance by setting initial values. - /// public override void Init() { base.Init(); @@ -98,10 +87,6 @@ public class Min : AbstractBase _timeSinceNewMin = 0; } - /// - /// Manages the state of the Min instance based on whether a new value is being processed. - /// - /// Indicates whether the current input is a new value. protected override void ManageState(bool isNew) { if (isNew) @@ -119,29 +104,23 @@ public class Min : AbstractBase } } - /// - /// Performs the minimum value calculation with decay. - /// - /// The calculated minimum value for the current period. - /// - /// This method updates the current minimum value based on the input, applies the decay - /// factor, and ensures the result is not lower than the actual minimum in the buffer. - /// The decay rate is calculated using an exponential function based on the time since - /// the last new minimum and the specified half-life. - /// protected override double Calculation() { ManageState(Input.IsNew); _buffer.Add(Input.Value, Input.IsNew); + // Update minimum if new value is lower if (Input.Value <= _currentMin) { _currentMin = Input.Value; _timeSinceNewMin = 0; } + // Apply decay based on time since last minimum double decayRate = 1 - Math.Exp(-_halfLife * _timeSinceNewMin / Period); _currentMin += decayRate * (_buffer.Average() - _currentMin); + + // Ensure minimum doesn't fall below current period's lowest value _currentMin = Math.Max(_currentMin, _buffer.Min()); IsHot = true; diff --git a/lib/statistics/Mode.cs b/lib/statistics/Mode.cs index a12a1fd6..91629638 100644 --- a/lib/statistics/Mode.cs +++ b/lib/statistics/Mode.cs @@ -1,34 +1,52 @@ +using System; +using System.Linq; namespace QuanTAlib; /// -/// Represents a mode calculator that determines the most frequent value in a specified period. -/// If multiple values have the same highest frequency, it returns their average. +/// MODE: Most Frequent Value Measure +/// A statistical measure that identifies the most frequently occurring value(s) +/// in a dataset. When multiple values share the highest frequency, it returns +/// their average to provide a representative central value. /// /// -/// The Mode class uses a circular buffer to store values and calculates the mode -/// efficiently. Before the specified period is reached, it returns the average of -/// the available values as an approximation. +/// The Mode calculation process: +/// 1. Groups values by frequency +/// 2. Identifies highest frequency group(s) +/// 3. Averages multiple modes if present +/// 4. Uses mean until period filled /// -/// In financial analysis, the mode can be useful for: -/// - Identifying the most common price levels, which could indicate support or resistance. -/// - Analyzing the distribution of returns or other financial metrics. -/// - Detecting patterns in trading volume or other discrete financial data. +/// Key characteristics: +/// - Identifies most common values +/// - Handles multiple modes +/// - Robust to distribution shape +/// - Useful for discrete data +/// - Returns actual data points +/// +/// Formula: +/// mode = value with highest frequency count +/// if multiple modes: average of mode values +/// +/// Market Applications: +/// - Identify common price levels +/// - Detect support/resistance zones +/// - Analyze volume clusters +/// - Find price congestion areas +/// - Pattern recognition +/// +/// Sources: +/// https://en.wikipedia.org/wiki/Mode_(statistics) +/// "Statistical Analysis in Financial Markets" +/// +/// Note: Particularly useful for price level analysis /// + public class Mode : AbstractBase { - /// - /// The number of data points to consider for the mode calculation. - /// private readonly int Period; private readonly CircularBuffer _buffer; - /// - /// Initializes a new instance of the Mode class with the specified period. - /// - /// The period over which to calculate the mode. - /// - /// Thrown when period is less than 1. - /// + /// The number of points to consider for mode calculation. + /// Thrown when period is less than 1. public Mode(int period) { if (period < 1) @@ -42,30 +60,20 @@ public class Mode : AbstractBase Init(); } - /// - /// Initializes a new instance of the Mode class with the specified source and period. - /// - /// The source object to subscribe to for value updates. - /// The period over which to calculate the mode. + /// The data source object that publishes updates. + /// The number of points to consider for mode calculation. public Mode(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - /// - /// Resets the Mode indicator to its initial state. - /// public override void Init() { base.Init(); _buffer.Clear(); } - /// - /// Manages the state of the Mode instance based on whether a new value is being processed. - /// - /// Indicates whether the current input is a new value. protected override void ManageState(bool isNew) { if (isNew) @@ -75,18 +83,6 @@ public class Mode : AbstractBase } } - /// - /// Performs the mode calculation for the current period. - /// - /// - /// The calculated mode (most frequent value) for the current period. - /// If multiple values have the same highest frequency, returns their average. - /// - /// - /// Before the specified period is reached, this method returns the average of - /// the available values as an approximation of the mode. Once the period is - /// reached, it calculates the true mode by grouping and counting the values. - /// protected override double Calculation() { ManageState(Input.IsNew); @@ -95,22 +91,26 @@ public class Mode : AbstractBase double mode; if (_index >= Period) { + // Group values by frequency and order by count var values = _buffer.GetSpan().ToArray(); var groupedValues = values.GroupBy(v => v) - .OrderByDescending(g => g.Count()) - .ThenBy(g => g.Key) - .ToList(); + .OrderByDescending(g => g.Count()) + .ThenBy(g => g.Key) + .ToList(); + // Find all values with highest frequency int maxCount = groupedValues.First().Count(); var modes = groupedValues.TakeWhile(g => g.Count() == maxCount) - .Select(g => g.Key) - .ToList(); + .Select(g => g.Key) + .ToList(); - mode = modes.Average(); // If there are multiple modes, we return their average + // Average multiple modes if present + mode = modes.Average(); } else { - mode = _buffer.Average(); // Use average until we have enough data points + // Use average until we have enough data points + mode = _buffer.Average(); } IsHot = _index >= WarmupPeriod; diff --git a/lib/statistics/Percentile.cs b/lib/statistics/Percentile.cs index ad1bdba6..4978a6ae 100644 --- a/lib/statistics/Percentile.cs +++ b/lib/statistics/Percentile.cs @@ -1,40 +1,54 @@ +using System; +using System.Linq; namespace QuanTAlib; /// -/// Represents a percentile calculator that determines the value at a specified percentile -/// in a given period of data points. +/// Percentile: Distribution Position Measure +/// A statistical measure that indicates the value below which a given percentage +/// of observations falls. Percentiles provide insights into data distribution +/// and are particularly useful for risk assessment and outlier detection. /// /// -/// The Percentile class uses a circular buffer to store values and calculates the -/// percentile efficiently. It uses linear interpolation when the percentile falls -/// between two data points. Before the specified period is reached, it returns the -/// average of the available values as an approximation. +/// The Percentile calculation process: +/// 1. Sorts values in ascending order +/// 2. Calculates position based on percentile +/// 3. Interpolates between adjacent values +/// 4. Uses mean until period filled /// -/// In financial analysis, percentiles are useful for: -/// - Assessing the relative standing of a value within a distribution. -/// - Identifying outliers or extreme values in financial data. -/// - Creating risk measures, such as Value at Risk (VaR) calculations. -/// - Analyzing the distribution of returns, trading volumes, or other financial metrics. +/// Key characteristics: +/// - Range specific value identification +/// - Linear interpolation for precision +/// - Distribution independent +/// - Robust to outliers +/// - Useful for risk metrics +/// +/// Formula: +/// position = (percentile/100) * (n-1) +/// value = v[floor(pos)] + (v[ceil(pos)] - v[floor(pos)]) * (pos - floor(pos)) +/// where n = number of observations, v = sorted values +/// +/// Market Applications: +/// - Value at Risk (VaR) calculation +/// - Risk management metrics +/// - Performance analysis +/// - Volatility assessment +/// - Outlier detection +/// +/// Sources: +/// https://en.wikipedia.org/wiki/Percentile +/// "Risk Management in Trading" - Davis Edwards +/// +/// Note: Particularly useful for risk metrics like VaR /// + public class Percentile : AbstractBase { - /// - /// The number of data points to consider for the percentile calculation. - /// private readonly int Period; - - /// - /// The percentile to calculate (between 0 and 100). - /// private readonly double Percent; - private readonly CircularBuffer _buffer; - /// - /// Initializes a new instance of the Percentile class with the specified period and percentile. - /// - /// The period over which to calculate the percentile. - /// The percentile to calculate (between 0 and 100). + /// The number of points to consider for percentile calculation. + /// The percentile to calculate (0-100). /// /// Thrown when period is less than 2 or percent is not between 0 and 100. /// @@ -42,11 +56,13 @@ public class Percentile : AbstractBase { if (period < 2) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2 for percentile calculation."); + throw new ArgumentOutOfRangeException(nameof(period), + "Period must be greater than or equal to 2 for percentile calculation."); } if (percent < 0 || percent > 100) { - throw new ArgumentOutOfRangeException(nameof(percent), "Percent must be between 0 and 100."); + throw new ArgumentOutOfRangeException(nameof(percent), + "Percent must be between 0 and 100."); } Period = period; Percent = percent; @@ -56,31 +72,21 @@ public class Percentile : AbstractBase Init(); } - /// - /// Initializes a new instance of the Percentile class with the specified source, period, and percentile. - /// - /// The source object to subscribe to for value updates. - /// The period over which to calculate the percentile. - /// The percentile to calculate (between 0 and 100). + /// The data source object that publishes updates. + /// The number of points to consider for percentile calculation. + /// The percentile to calculate (0-100). public Percentile(object source, int period, double percent) : this(period, percent) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - /// - /// Initializes the Percentile instance by clearing the buffer. - /// public override void Init() { base.Init(); _buffer.Clear(); } - /// - /// Manages the state of the Percentile instance based on whether a new value is being processed. - /// - /// Indicates whether the current input is a new value. protected override void ManageState(bool isNew) { if (isNew) @@ -90,18 +96,6 @@ public class Percentile : AbstractBase } } - /// - /// Performs the percentile calculation for the current period. - /// - /// - /// The calculated percentile value for the current period. - /// - /// - /// This method uses linear interpolation when the percentile falls between two data points. - /// Before the specified period is reached, it returns the average of the available values - /// as an approximation. Once the period is reached, it calculates the true percentile by - /// sorting the values and interpolating as necessary. - /// protected override double Calculation() { ManageState(Input.IsNew); @@ -110,6 +104,7 @@ public class Percentile : AbstractBase double result; if (_buffer.Count >= Period) { + // Sort values and calculate percentile position var values = _buffer.GetSpan().ToArray(); Array.Sort(values); @@ -123,7 +118,7 @@ public class Percentile : AbstractBase } else { - // Interpolate between the two nearest values + // Linear interpolation between adjacent values double lowerValue = values[lowerIndex]; double upperValue = values[upperIndex]; double fraction = position - lowerIndex; @@ -132,7 +127,7 @@ public class Percentile : AbstractBase } else { - // Use average for insufficient data, like the Median class + // Use average until we have enough data points result = _buffer.Average(); } diff --git a/lib/statistics/Skew.cs b/lib/statistics/Skew.cs index 75711882..3ab7db22 100644 --- a/lib/statistics/Skew.cs +++ b/lib/statistics/Skew.cs @@ -1,44 +1,62 @@ +using System; +using System.Linq; namespace QuanTAlib; /// -/// Represents a skewness calculator that measures the asymmetry of the probability -/// distribution of a real-valued random variable about its mean. +/// SKEW: Distribution Asymmetry Measure +/// A statistical measure that quantifies the asymmetry of a probability distribution +/// around its mean. Skewness indicates whether deviations from the mean are more +/// likely in one direction than the other. /// /// -/// The Skew class uses a circular buffer to store values and calculates the skewness -/// efficiently. It uses the adjusted Fisher-Pearson standardized moment coefficient -/// for sample skewness calculation. A minimum of 3 data points is required for the -/// calculation. +/// The Skew calculation process: +/// 1. Calculates mean of the data +/// 2. Computes deviations from mean +/// 3. Calculates third moment (cubed deviations) +/// 4. Normalizes by standard deviation cubed /// -/// In financial analysis, skewness is important for: -/// - Assessing the asymmetry of returns distribution. -/// - Evaluating the risk of extreme events in either direction. -/// - Complementing other risk measures like standard deviation. -/// - Informing investment decisions and risk management strategies. +/// Key characteristics: +/// - Measures distribution asymmetry +/// - Positive values indicate right skew +/// - Negative values indicate left skew +/// - Zero indicates symmetry +/// - Scale-independent measure /// -/// Positive skewness indicates a longer tail on the right side of the distribution, -/// while negative skewness indicates a longer tail on the left side. +/// Formula: +/// skew = [√(n(n-1))/(n-2)] * [m₃/s³] +/// where: +/// m₃ = third moment about the mean +/// s = standard deviation +/// n = sample size +/// +/// Market Applications: +/// - Risk assessment in returns +/// - Options pricing models +/// - Trading strategy development +/// - Portfolio risk management +/// - Market sentiment analysis +/// +/// Sources: +/// Fisher-Pearson standardized moment coefficient +/// https://en.wikipedia.org/wiki/Skewness +/// "The Analysis of Financial Time Series" - Ruey S. Tsay +/// +/// Note: Requires minimum of 3 data points for calculation /// + public class Skew : AbstractBase { - /// - /// The number of data points to consider for the skewness calculation. - /// private readonly int Period; private readonly CircularBuffer _buffer; - /// - /// Initializes a new instance of the Skew class with the specified period. - /// - /// The period over which to calculate the skewness. - /// - /// Thrown when period is less than 3. - /// + /// The number of points to consider for skewness calculation. + /// Thrown when period is less than 3. public Skew(int period) { if (period < 3) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 3 for skewness calculation."); + throw new ArgumentOutOfRangeException(nameof(period), + "Period must be greater than or equal to 3 for skewness calculation."); } Period = period; WarmupPeriod = 3; @@ -47,30 +65,20 @@ public class Skew : AbstractBase Init(); } - /// - /// Initializes a new instance of the Skew class with the specified source and period. - /// - /// The source object to subscribe to for value updates. - /// The period over which to calculate the skewness. + /// The data source object that publishes updates. + /// The number of points to consider for skewness calculation. public Skew(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - /// - /// Initializes the Skew instance by clearing the buffer. - /// public override void Init() { base.Init(); _buffer.Clear(); } - /// - /// Manages the state of the Skew instance based on whether a new value is being processed. - /// - /// Indicates whether the current input is a new value. protected override void ManageState(bool isNew) { if (isNew) @@ -80,36 +88,19 @@ public class Skew : AbstractBase } } - /// - /// Performs the skewness calculation for the current period. - /// - /// - /// The calculated skewness value for the current period. - /// - /// - /// This method uses the adjusted Fisher-Pearson standardized moment coefficient - /// to calculate the sample skewness. It requires at least 3 data points for the - /// calculation. If there are fewer than 3 data points, or if the standard - /// deviation is zero, the method returns 0. - /// - /// Interpretation of results: - /// - Positive values indicate right-skewed distribution (longer tail on the right side). - /// - Negative values indicate left-skewed distribution (longer tail on the left side). - /// - Values close to 0 suggest a relatively symmetric distribution. - /// protected override double Calculation() { ManageState(Input.IsNew); - _buffer.Add(Input.Value, Input.IsNew); double skew = 0; - if (_buffer.Count >= 3) - { // We need at least 3 data points for skewness + if (_buffer.Count >= 3) // Need at least 3 points for skewness + { var values = _buffer.GetSpan().ToArray(); double mean = values.Average(); double n = values.Length; + // Calculate third and second moments double sumCubedDeviations = 0; double sumSquaredDeviations = 0; @@ -120,13 +111,13 @@ public class Skew : AbstractBase sumSquaredDeviations += Math.Pow(deviation, 2); } - // Calculate sample skewness using the adjusted Fisher-Pearson standardized moment coefficient + // Fisher-Pearson standardized moment coefficient double m3 = sumCubedDeviations / n; double m2 = sumSquaredDeviations / n; double s3 = Math.Pow(m2, 1.5); - if (s3 != 0) - { // Avoid division by zero + if (s3 != 0) // Avoid division by zero + { skew = (Math.Sqrt(n * (n - 1)) / (n - 2)) * (m3 / s3); } } diff --git a/lib/statistics/Slope.cs b/lib/statistics/Slope.cs index e8681069..8731e507 100644 --- a/lib/statistics/Slope.cs +++ b/lib/statistics/Slope.cs @@ -1,52 +1,68 @@ +using System; +using System.Linq; namespace QuanTAlib; /// -/// Represents a slope calculator that performs linear regression on a series of data points. +/// SLOPE: Linear Regression Trend Measure +/// A statistical measure that calculates the rate of change using linear regression. +/// Slope indicates the direction and steepness of a trend, providing insights into +/// momentum and potential trend changes. /// /// -/// The Slope class calculates the slope of a linear regression line, along with other -/// statistical measures such as intercept, standard deviation, R-squared, and the last -/// point on the regression line. It uses the least squares method for calculation. +/// The Slope calculation process: +/// 1. Calculates means of x and y values +/// 2. Computes deviations from means +/// 3. Applies least squares method +/// 4. Provides additional regression statistics /// -/// In financial analysis, slope is important for: -/// - Identifying trends in price movements or other financial metrics. -/// - Measuring the rate of change in a financial time series. -/// - Assessing the strength and direction of relationships between variables. -/// - Supporting technical analysis indicators and trading strategies. +/// Key characteristics: +/// - Measures trend direction and strength +/// - Provides rate of change +/// - Scale-dependent measure +/// - Includes regression statistics +/// - Time-weighted calculation +/// +/// Formula: +/// slope = Σ((x - x̄)(y - ȳ)) / Σ((x - x̄)²) +/// where: +/// x = time points +/// y = price values +/// x̄, ȳ = respective means +/// +/// Market Applications: +/// - Trend identification +/// - Momentum measurement +/// - Support/resistance angles +/// - Price target projection +/// - Trend strength analysis +/// +/// Sources: +/// https://en.wikipedia.org/wiki/Simple_linear_regression +/// "Technical Analysis of Financial Markets" - John J. Murphy +/// +/// Note: Provides additional regression statistics (R², intercept) /// + public class Slope : AbstractBase { private readonly int _period; private readonly CircularBuffer _buffer; private readonly CircularBuffer _timeBuffer; - /// - /// Gets the y-intercept of the regression line. - /// + /// Gets the y-intercept of the regression line. public double? Intercept { get; private set; } - /// - /// Gets the standard deviation of the y-values. - /// + /// Gets the standard deviation of the y-values. public double? StdDev { get; private set; } - /// - /// Gets the R-squared value, indicating the goodness of fit of the regression line. - /// + /// Gets the R-squared value, indicating regression fit quality. public double? RSquared { get; private set; } - /// - /// Gets the y-value of the last point on the regression line. - /// + /// Gets the last point on the regression line. public double? Line { get; private set; } - /// - /// Initializes a new instance of the Slope class with the specified period. - /// - /// The period over which to calculate the slope. - /// - /// Thrown when period is less than or equal to 1. - /// + /// The number of points to consider for slope calculation. + /// Thrown when period is less than or equal to 1. public Slope(int period) { if (period <= 1) @@ -59,24 +75,17 @@ public class Slope : AbstractBase _buffer = new CircularBuffer(period); _timeBuffer = new CircularBuffer(period); Name = $"Slope(period={period})"; - Init(); } - /// - /// Initializes a new instance of the Slope class with the specified source and period. - /// - /// The source object to subscribe to for value updates. - /// The period over which to calculate the slope. + /// The data source object that publishes updates. + /// The number of points to consider for slope calculation. public Slope(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - /// - /// Initializes the Slope instance by clearing buffers and resetting calculated values. - /// public override void Init() { base.Init(); @@ -88,10 +97,6 @@ public class Slope : AbstractBase Line = null; } - /// - /// Manages the state of the Slope instance based on whether a new value is being processed. - /// - /// Indicates whether the current input is a new value. protected override void ManageState(bool isNew) { if (isNew) @@ -101,25 +106,6 @@ public class Slope : AbstractBase } } - /// - /// Performs the slope calculation using linear regression for the current period. - /// - /// - /// The calculated slope value for the current period. - /// - /// - /// This method uses the least squares method to calculate the slope of the regression line. - /// It also calculates and updates the Intercept, StdDev, RSquared, and Line properties. - /// If there are fewer than 2 data points, or if the sum of squared x deviations is 0, - /// the method returns 0 and sets the additional properties to null. - /// - /// Interpretation of results: - /// - Positive slope: Indicates an upward trend in the data. - /// - Negative slope: Indicates a downward trend in the data. - /// - Slope close to 0: Indicates a relatively flat or no clear trend in the data. - /// The magnitude of the slope represents the rate of change in the dependent variable - /// (y) for each unit change in the independent variable (x). - /// protected override double Calculation() { ManageState(Input.IsNew); @@ -128,10 +114,9 @@ public class Slope : AbstractBase _timeBuffer.Add(Input.Time.Ticks, Input.IsNew); double slope = 0; - if (_buffer.Count < 2) { - return slope; // Return 0 when there are fewer than 2 points + return slope; // Need at least 2 points } int count = Math.Min(_buffer.Count, _period); @@ -147,7 +132,7 @@ public class Slope : AbstractBase double avgX = sumX / count; double avgY = sumY / count; - // Least squares method + // Least squares regression double sumSqX = 0, sumSqY = 0, sumSqXY = 0; for (int i = 0; i < count; i++) { @@ -160,6 +145,7 @@ public class Slope : AbstractBase if (sumSqX > 0) { + // Calculate slope and related statistics slope = sumSqXY / sumSqX; Intercept = avgY - (slope * avgX); @@ -174,7 +160,7 @@ public class Slope : AbstractBase RSquared = r * r; } - // Calculate last Line value (y = mx + b) + // Calculate regression line endpoint Line = (slope * count) + Intercept; } else diff --git a/lib/statistics/Stddev.cs b/lib/statistics/Stddev.cs index aae6fb20..47433a91 100644 --- a/lib/statistics/Stddev.cs +++ b/lib/statistics/Stddev.cs @@ -1,48 +1,63 @@ +using System; +using System.Linq; namespace QuanTAlib; /// -/// Represents a standard deviation calculator that measures the amount of variation or -/// dispersion of a set of values. +/// STDDEV: Standard Deviation Volatility Measure +/// A statistical measure that quantifies the amount of variation or dispersion +/// in a dataset. Standard deviation is widely used in finance as a measure of +/// volatility and risk assessment. /// /// -/// The Stddev class calculates either the population standard deviation or the sample -/// standard deviation based on the isPopulation parameter. It uses a circular buffer -/// to efficiently manage the data points within the specified period. +/// The StdDev calculation process: +/// 1. Calculates mean of the data +/// 2. Computes squared deviations from mean +/// 3. Averages squared deviations +/// 4. Takes square root of average /// -/// In financial analysis, standard deviation is important for: -/// - Measuring volatility of financial instruments or portfolios. -/// - Assessing risk in investments. -/// - Calculating Sharpe ratios and other risk-adjusted performance measures. -/// - Identifying potential outliers or unusual market behavior. +/// Key characteristics: +/// - Measures data dispersion +/// - Same units as input data +/// - Sensitive to outliers +/// - Population or sample versions +/// - Key volatility indicator +/// +/// Formula: +/// Population: σ = √(Σ(x - μ)² / N) +/// Sample: s = √(Σ(x - x̄)² / (n-1)) +/// where: +/// x = values +/// μ, x̄ = mean +/// N, n = count +/// +/// Market Applications: +/// - Volatility measurement +/// - Risk assessment +/// - Bollinger Bands +/// - Option pricing +/// - Portfolio management +/// +/// Sources: +/// https://en.wikipedia.org/wiki/Standard_deviation +/// "Options, Futures, and Other Derivatives" - John C. Hull +/// +/// Note: Foundation for many volatility-based indicators /// + public class Stddev : AbstractBase { - /// - /// Indicates whether to calculate population (true) or sample (false) standard deviation. - /// private readonly bool IsPopulation; - - /// - /// Circular buffer to store the most recent data points. - /// private readonly CircularBuffer _buffer; - /// - /// Initializes a new instance of the Stddev class with the specified period and - /// population flag. - /// - /// The period over which to calculate the standard deviation. - /// - /// A flag indicating whether to calculate population (true) or sample (false) standard deviation. - /// - /// - /// Thrown when period is less than 2. - /// + /// The number of points to consider for standard deviation calculation. + /// True for population stddev, false for sample stddev (default). + /// Thrown when period is less than 2. public Stddev(int period, bool isPopulation = false) { if (period < 2) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2."); + throw new ArgumentOutOfRangeException(nameof(period), + "Period must be greater than or equal to 2."); } IsPopulation = isPopulation; WarmupPeriod = 0; @@ -51,34 +66,21 @@ public class Stddev : AbstractBase Init(); } - /// - /// Initializes a new instance of the Stddev class with the specified source, period, - /// and population flag. - /// - /// The source object to subscribe to for value updates. - /// The period over which to calculate the standard deviation. - /// - /// A flag indicating whether to calculate population (true) or sample (false) standard deviation. - /// + /// The data source object that publishes updates. + /// The number of points to consider for standard deviation calculation. + /// True for population stddev, false for sample stddev (default). public Stddev(object source, int period, bool isPopulation = false) : this(period, isPopulation) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - /// - /// Initializes the Stddev instance by clearing the buffer. - /// public override void Init() { base.Init(); _buffer.Clear(); } - /// - /// Manages the state of the Stddev instance based on whether a new value is being processed. - /// - /// Indicates whether the current input is a new value. protected override void ManageState(bool isNew) { if (isNew) @@ -88,28 +90,9 @@ public class Stddev : AbstractBase } } - /// - /// Performs the standard deviation calculation for the current period. - /// - /// - /// The calculated standard deviation value for the current period. - /// - /// - /// This method calculates the standard deviation using the formula: - /// sqrt(sum((x - mean)^2) / n) for population, or - /// sqrt(sum((x - mean)^2) / (n - 1)) for sample, - /// where x is each value, mean is the average of all values, and n is the number of values. - /// If there's only one value in the buffer, the method returns 0. - /// - /// Interpretation of results: - /// - A low standard deviation indicates that the values tend to be close to the mean. - /// - A high standard deviation indicates that the values are spread out over a wider range. - /// - In financial contexts, higher standard deviation often implies higher volatility or risk. - /// protected override double Calculation() { ManageState(Input.IsNew); - _buffer.Add(Input.Value, Input.IsNew); double stddev = 0; @@ -117,8 +100,11 @@ public class Stddev : AbstractBase { var values = _buffer.GetSpan().ToArray(); double mean = values.Average(); + + // Calculate sum of squared deviations double sumOfSquaredDifferences = values.Sum(x => Math.Pow(x - mean, 2)); + // Use appropriate divisor based on population/sample calculation double divisor = IsPopulation ? _buffer.Count : _buffer.Count - 1; double variance = sumOfSquaredDifferences / divisor; stddev = Math.Sqrt(variance); diff --git a/lib/statistics/Variance.cs b/lib/statistics/Variance.cs index e1bc4c67..12916507 100644 --- a/lib/statistics/Variance.cs +++ b/lib/statistics/Variance.cs @@ -1,48 +1,63 @@ +using System; +using System.Linq; namespace QuanTAlib; /// -/// Represents a variance calculator that measures the spread of a set of numbers -/// from their average value. +/// VARIANCE: Squared Deviation Risk Measure +/// A statistical measure that quantifies the spread of data points around their +/// mean value. Variance is fundamental to risk assessment and portfolio theory, +/// providing the basis for many financial models. /// /// -/// The Variance class calculates either the population variance or the sample -/// variance based on the isPopulation parameter. It uses a circular buffer -/// to efficiently manage the data points within the specified period. +/// The Variance calculation process: +/// 1. Calculates mean of the data +/// 2. Computes squared deviations from mean +/// 3. Sums squared deviations +/// 4. Divides by n or (n-1) /// -/// In financial analysis, variance is important for: -/// - Measuring the dispersion of returns around the mean. -/// - Assessing risk and volatility in financial instruments or portfolios. -/// - Serving as a basis for other risk measures like standard deviation and beta. -/// - Contributing to portfolio optimization techniques, such as Modern Portfolio Theory. +/// Key characteristics: +/// - Measures data dispersion +/// - Squared units of input data +/// - Always non-negative +/// - Population or sample versions +/// - Foundation for risk metrics +/// +/// Formula: +/// Population: σ² = Σ(x - μ)² / N +/// Sample: s² = Σ(x - x̄)² / (n-1) +/// where: +/// x = values +/// μ, x̄ = mean +/// N, n = count +/// +/// Market Applications: +/// - Portfolio optimization +/// - Risk measurement +/// - Modern Portfolio Theory +/// - Asset allocation +/// - Volatility analysis +/// +/// Sources: +/// Harry Markowitz - "Portfolio Selection" (1952) +/// https://en.wikipedia.org/wiki/Variance +/// +/// Note: Basis for Modern Portfolio Theory and risk models /// + public class Variance : AbstractBase { - /// - /// Indicates whether to calculate population (true) or sample (false) variance. - /// private readonly bool IsPopulation; - - /// - /// Circular buffer to store the most recent data points. - /// private readonly CircularBuffer _buffer; - /// - /// Initializes a new instance of the Variance class with the specified period and - /// population flag. - /// - /// The period over which to calculate the variance. - /// - /// A flag indicating whether to calculate population (true) or sample (false) variance. - /// - /// - /// Thrown when period is less than 2. - /// + /// The number of points to consider for variance calculation. + /// True for population variance, false for sample variance (default). + /// Thrown when period is less than 2. public Variance(int period, bool isPopulation = false) { if (period < 2) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2."); + throw new ArgumentOutOfRangeException(nameof(period), + "Period must be greater than or equal to 2."); } IsPopulation = isPopulation; WarmupPeriod = 0; @@ -51,34 +66,21 @@ public class Variance : AbstractBase Init(); } - /// - /// Initializes a new instance of the Variance class with the specified source, period, - /// and population flag. - /// - /// The source object to subscribe to for value updates. - /// The period over which to calculate the variance. - /// - /// A flag indicating whether to calculate population (true) or sample (false) variance. - /// + /// The data source object that publishes updates. + /// The number of points to consider for variance calculation. + /// True for population variance, false for sample variance (default). public Variance(object source, int period, bool isPopulation = false) : this(period, isPopulation) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - /// - /// Initializes the Variance instance by clearing the buffer. - /// public override void Init() { base.Init(); _buffer.Clear(); } - /// - /// Manages the state of the Variance instance based on whether a new value is being processed. - /// - /// Indicates whether the current input is a new value. protected override void ManageState(bool isNew) { if (isNew) @@ -88,29 +90,9 @@ public class Variance : AbstractBase } } - /// - /// Performs the variance calculation for the current period. - /// - /// - /// The calculated variance value for the current period. - /// - /// - /// This method calculates the variance using the formula: - /// sum((x - mean)^2) / n for population, or - /// sum((x - mean)^2) / (n - 1) for sample, - /// where x is each value, mean is the average of all values, and n is the number of values. - /// If there's only one value in the buffer, the method returns 0. - /// - /// Interpretation of results: - /// - A low variance indicates that the values tend to be close to the mean and to each other. - /// - A high variance indicates that the values are spread out over a wider range. - /// - In financial contexts, higher variance often implies higher volatility or risk. - /// - Variance is always non-negative, and its units are squared units of the original data. - /// protected override double Calculation() { ManageState(Input.IsNew); - _buffer.Add(Input.Value, Input.IsNew); double variance = 0; @@ -118,8 +100,11 @@ public class Variance : AbstractBase { var values = _buffer.GetSpan().ToArray(); double mean = values.Average(); + + // Calculate sum of squared deviations double sumOfSquaredDifferences = values.Sum(x => Math.Pow(x - mean, 2)); + // Use appropriate divisor based on population/sample calculation double divisor = IsPopulation ? _buffer.Count : _buffer.Count - 1; variance = sumOfSquaredDifferences / divisor; } diff --git a/lib/statistics/Zscore.cs b/lib/statistics/Zscore.cs index de94054c..c3c72769 100644 --- a/lib/statistics/Zscore.cs +++ b/lib/statistics/Zscore.cs @@ -1,44 +1,61 @@ +using System; +using System.Linq; namespace QuanTAlib; /// -/// Represents a Z-score calculator that measures how many standard deviations -/// an element is from the mean of a set of values. +/// ZSCORE: Standardized Distance Measure +/// A statistical measure that indicates how many standard deviations an observation +/// is from the mean. Z-scores normalize data to a standard scale, making it useful +/// for comparing values across different distributions. /// /// -/// The Zscore class calculates the Z-score (also known as standard score) for -/// the most recent value in a given period. It uses a circular buffer to -/// efficiently manage the data points within the specified period. +/// The Zscore calculation process: +/// 1. Calculates mean of the period +/// 2. Computes standard deviation +/// 3. Measures distance from mean +/// 4. Normalizes by standard deviation /// -/// In financial analysis, Z-score is important for: -/// - Identifying outliers or unusual price movements. -/// - Normalizing data across different scales or time periods. -/// - Assessing the relative position of a value within its historical distribution. -/// - Supporting trading strategies based on mean reversion or momentum. +/// Key characteristics: +/// - Scale-independent measure +/// - Symmetric around zero +/// - Normal distribution context +/// - Outlier identification +/// - Comparative analysis tool +/// +/// Formula: +/// Z = (x - μ) / σ +/// where: +/// x = current value +/// μ = mean +/// σ = standard deviation +/// +/// Market Applications: +/// - Mean reversion strategies +/// - Overbought/oversold signals +/// - Volatility breakouts +/// - Cross-asset comparison +/// - Statistical arbitrage +/// +/// Sources: +/// https://en.wikipedia.org/wiki/Standard_score +/// "Statistical Analysis in Trading" - Technical Analysis +/// +/// Note: Assumes approximately normal distribution /// + public class Zscore : AbstractBase { - /// - /// The number of data points to consider for the Z-score calculation. - /// private readonly int Period; - - /// - /// Circular buffer to store the most recent data points. - /// private readonly CircularBuffer _buffer; - /// - /// Initializes a new instance of the Zscore class with the specified period. - /// - /// The period over which to calculate the Z-score. - /// - /// Thrown when period is less than 2. - /// + /// The number of points to consider for Z-score calculation. + /// Thrown when period is less than 2. public Zscore(int period) { if (period < 2) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2 for Z-score calculation."); + throw new ArgumentOutOfRangeException(nameof(period), + "Period must be greater than or equal to 2 for Z-score calculation."); } Period = period; WarmupPeriod = 2; @@ -47,30 +64,20 @@ public class Zscore : AbstractBase Init(); } - /// - /// Initializes a new instance of the Zscore class with the specified source and period. - /// - /// The source object to subscribe to for value updates. - /// The period over which to calculate the Z-score. + /// The data source object that publishes updates. + /// The number of points to consider for Z-score calculation. public Zscore(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - /// - /// Initializes the Zscore instance by clearing the buffer. - /// public override void Init() { base.Init(); _buffer.Clear(); } - /// - /// Manages the state of the Zscore instance based on whether a new value is being processed. - /// - /// Indicates whether the current input is a new value. protected override void ManageState(bool isNew) { if (isNew) @@ -80,44 +87,24 @@ public class Zscore : AbstractBase } } - /// - /// Performs the Z-score calculation for the current period. - /// - /// - /// The calculated Z-score value for the most recent input in the current period. - /// - /// - /// This method calculates the Z-score using the formula: - /// Z = (x - μ) / σ - /// where x is the input value, μ is the mean of the period, and σ is the sample standard deviation. - /// If there are fewer than 2 data points or if the standard deviation is 0, the method returns 0. - /// - /// Interpretation of results: - /// - A Z-score of 0 indicates that the data point is exactly on the mean. - /// - A positive Z-score indicates the data point is above the mean. - /// - A negative Z-score indicates the data point is below the mean. - /// - The magnitude of the Z-score represents how many standard deviations away from the mean the data point is. - /// - In a normal distribution, about 68% of the values have a Z-score between -1 and 1, - /// 95% between -2 and 2, and 99.7% between -3 and 3. - /// protected override double Calculation() { ManageState(Input.IsNew); - _buffer.Add(Input.Value, Input.IsNew); double zScore = 0; - if (_buffer.Count >= 2) - { // We need at least 2 data points for Z-score + if (_buffer.Count >= 2) // Need at least 2 points for standard deviation + { var values = _buffer.GetSpan().ToArray(); double mean = values.Average(); double n = values.Length; + // Calculate sample standard deviation double sumSquaredDeviations = values.Sum(x => Math.Pow(x - mean, 2)); - double standardDeviation = Math.Sqrt(sumSquaredDeviations / (n - 1)); // Sample standard deviation + double standardDeviation = Math.Sqrt(sumSquaredDeviations / (n - 1)); - if (standardDeviation != 0) - { // Avoid division by zero + if (standardDeviation != 0) // Avoid division by zero + { zScore = (Input.Value - mean) / standardDeviation; } } diff --git a/lib/volatility/Atr.cs b/lib/volatility/Atr.cs index 3ea3e5f0..43d1983a 100644 --- a/lib/volatility/Atr.cs +++ b/lib/volatility/Atr.cs @@ -1,51 +1,75 @@ +using System; namespace QuanTAlib; /// -/// Represents an Average True Range (ATR) calculator, a measure of market volatility. +/// ATR: Average True Range +/// A technical indicator that measures market volatility by decomposing the entire +/// range of an asset's price for a period. ATR accounts for gaps between periods +/// and provides a comprehensive view of price volatility. /// /// -/// The ATR class calculates the average true range using a Relative Moving Average (RMA) -/// of the true range. The true range is the greatest of: current high - current low, -/// absolute value of current high - previous close, or absolute value of current low - previous close. +/// The ATR calculation process: +/// 1. Calculates True Range (TR) as maximum of: +/// - Current High - Current Low +/// - |Current High - Previous Close| +/// - |Current Low - Previous Close| +/// 2. Applies RMA smoothing to TR values +/// 3. Updates with each new price bar +/// 4. Adapts to changing volatility +/// +/// Key characteristics: +/// - Absolute price measure +/// - Gap-inclusive calculation +/// - Trend independent +/// - Volatility focused +/// - Smoothed output +/// +/// Formula: +/// TR = max(high-low, |high-prevClose|, |low-prevClose|) +/// ATR = RMA(TR, period) +/// +/// Market Applications: +/// - Position sizing +/// - Stop loss placement +/// - Volatility breakouts +/// - Risk assessment +/// - Entry/exit timing +/// +/// Sources: +/// J. Welles Wilder - "New Concepts in Technical Trading Systems" +/// https://www.investopedia.com/terms/a/atr.asp +/// +/// Note: Higher ATR indicates higher volatility /// + public class Atr : AbstractBase { public double Tr { get; private set; } private readonly Rma _ma; private double _prevClose, _p_prevClose; - /// - /// Initializes a new instance of the Atr class with the specified period. - /// - /// The period over which to calculate the ATR. - /// - /// Thrown when period is less than 1. - /// + /// The number of periods for ATR calculation. + /// Thrown when period is less than 1. public Atr(int period) { if (period < 1) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); + throw new ArgumentOutOfRangeException(nameof(period), + "Period must be greater than or equal to 1."); } _ma = new(period, useSma: true); WarmupPeriod = _ma.WarmupPeriod; Name = $"ATR({period})"; } - /// - /// Initializes a new instance of the Atr class with the specified source and period. - /// - /// The source object to subscribe to for bar updates. - /// The period over which to calculate the ATR. + /// The data source object that publishes updates. + /// The number of periods for ATR calculation. public Atr(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new BarSignal(Sub)); } - /// - /// Initializes the Atr instance by setting up the initial state. - /// public override void Init() { base.Init(); @@ -54,10 +78,6 @@ public class Atr : AbstractBase Tr = 0; } - /// - /// Manages the state of the Atr instance based on whether a new bar is being processed. - /// - /// Indicates whether the current input is a new bar. protected override void ManageState(bool isNew) { if (isNew) @@ -71,28 +91,19 @@ public class Atr : AbstractBase } } - /// - /// Performs the ATR calculation for the current bar. - /// - /// - /// The calculated ATR value for the current bar. - /// - /// - /// This method calculates the true range for the current bar and then uses an RMA - /// to smooth the true range values. For the first bar, it uses the high-low range - /// as the true range. - /// protected override double Calculation() { ManageState(BarInput.IsNew); if (_index == 1) { + // First bar uses simple high-low range Tr = BarInput.High - BarInput.Low; _prevClose = BarInput.Close; } else { + // Calculate True Range as maximum of three measures Tr = Math.Max( BarInput.High - BarInput.Low, Math.Max( @@ -101,6 +112,8 @@ public class Atr : AbstractBase ) ); } + + // Apply RMA smoothing to True Range _ma.Calc(new TValue(Input.Time, Tr, BarInput.IsNew)); IsHot = _ma.IsHot; diff --git a/lib/volatility/Hv.cs b/lib/volatility/Hv.cs index ef74e37e..f6c73cc7 100644 --- a/lib/volatility/Hv.cs +++ b/lib/volatility/Hv.cs @@ -1,14 +1,49 @@ +using System; +using System.Linq; namespace QuanTAlib; /// -/// Represents a historical volatility calculator that measures the dispersion of returns -/// for a given security or market index over a specific period. +/// HV: Historical Volatility +/// A statistical measure that calculates the dispersion of returns over time, +/// providing insights into past price variability. Historical volatility is +/// fundamental to options pricing and risk assessment. /// /// -/// The Historical class calculates volatility based on logarithmic returns. It can provide -/// both annualized and non-annualized volatility measures. The calculation uses a sample -/// standard deviation formula and assumes 252 trading days in a year for annualization. +/// The HV calculation process: +/// 1. Computes daily log returns +/// 2. Calculates standard deviation +/// 3. Annualizes if specified +/// 4. Uses sample variance formula +/// +/// Key characteristics: +/// - Backward-looking measure +/// - Log-return based +/// - Optional annualization +/// - Sample-based calculation +/// - Trading-day adjusted +/// +/// Formula: +/// HV = √[(Σ(ln(P[t]/P[t-1]) - μ)²)/(n-1)] * √252 +/// where: +/// P = price +/// μ = mean of log returns +/// n = number of observations +/// 252 = trading days per year +/// +/// Market Applications: +/// - Options pricing +/// - Risk assessment +/// - Trading ranges +/// - Portfolio management +/// - Volatility trading +/// +/// Sources: +/// Black-Scholes Option Pricing Model +/// https://en.wikipedia.org/wiki/Volatility_(finance) +/// +/// Note: Assumes 252 trading days for annualization /// + public class Hv : AbstractBase { private readonly int Period; @@ -17,44 +52,34 @@ public class Hv : AbstractBase private readonly CircularBuffer _logReturns; private double _previousClose; - /// - /// Initializes a new instance of the Historical class with the specified period and annualization flag. - /// - /// The period over which to calculate historical volatility. - /// Whether to annualize the volatility (default is true). - /// - /// Thrown when period is less than 2. - /// + /// The number of periods for volatility calculation. + /// Whether to annualize the result (default true). + /// Thrown when period is less than 2. public Hv(int period, bool isAnnualized = true) { if (period < 2) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2."); + throw new ArgumentOutOfRangeException(nameof(period), + "Period must be greater than or equal to 2."); } Period = period; IsAnnualized = isAnnualized; - WarmupPeriod = period + 1; // We need one extra data point to calculate the first return + WarmupPeriod = period + 1; // Need extra point for first return _buffer = new CircularBuffer(period + 1); _logReturns = new CircularBuffer(period); Name = $"Historical(period={period}, annualized={isAnnualized})"; Init(); } - /// - /// Initializes a new instance of the Historical class with the specified source, period, and annualization flag. - /// - /// The source object to subscribe to for value updates. - /// The period over which to calculate historical volatility. - /// Whether to annualize the volatility (default is true). + /// The data source object that publishes updates. + /// The number of periods for volatility calculation. + /// Whether to annualize the result (default true). public Hv(object source, int period, bool isAnnualized = true) : this(period, isAnnualized) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - /// - /// Initializes the Historical instance by clearing buffers and resetting the previous close value. - /// public override void Init() { base.Init(); @@ -63,10 +88,6 @@ public class Hv : AbstractBase _previousClose = 0; } - /// - /// Manages the state of the Historical instance based on whether a new value is being processed. - /// - /// Indicates whether the current input is a new value. protected override void ManageState(bool isNew) { if (isNew) @@ -76,47 +97,35 @@ public class Hv : AbstractBase } } - /// - /// Performs the historical volatility calculation for the current period. - /// - /// - /// The calculated historical volatility value for the current period. - /// - /// - /// This method calculates the volatility using the following steps: - /// 1. Compute logarithmic returns. - /// 2. Calculate the sample standard deviation of the log returns. - /// 3. If annualized, multiply by the square root of 252 (assumed trading days in a year). - /// The method returns 0 until enough data points are available for the calculation. - /// protected override double Calculation() { ManageState(Input.IsNew); - _buffer.Add(Input.Value, Input.IsNew); double volatility = 0; if (_buffer.Count > 1) { + // Calculate log return if we have previous close if (_previousClose != 0) { double logReturn = Math.Log(Input.Value / _previousClose); _logReturns.Add(logReturn, Input.IsNew); } + // Calculate volatility when we have enough returns if (_logReturns.Count == Period) { var returns = _logReturns.GetSpan().ToArray(); double mean = returns.Average(); double sumOfSquaredDifferences = returns.Sum(x => Math.Pow(x - mean, 2)); - double variance = sumOfSquaredDifferences / (Period - 1); // Using sample standard deviation + // Sample standard deviation + double variance = sumOfSquaredDifferences / (Period - 1); volatility = Math.Sqrt(variance); if (IsAnnualized) { - // Assuming 252 trading days in a year. Adjust as needed. - volatility *= Math.Sqrt(252); + volatility *= Math.Sqrt(252); // Annualize using trading days } } } diff --git a/lib/volatility/Jvolty.cs b/lib/volatility/Jvolty.cs index 23f6ae97..af7c305e 100644 --- a/lib/volatility/Jvolty.cs +++ b/lib/volatility/Jvolty.cs @@ -1,9 +1,46 @@ -/// -/// Represents a Jurik Volatility (Jvolty) calculator, a measure of market volatility based on Jurik Moving Average (JMA) concepts. -/// - +using System; namespace QuanTAlib; +/// +/// JVOLTY: Jurik Volatility +/// An advanced volatility measure developed by Mark Jurik that combines adaptive +/// bands with JMA smoothing. JVOLTY provides a sophisticated approach to measuring +/// market volatility with reduced noise and better responsiveness. +/// +/// +/// The JVOLTY calculation process: +/// 1. Calculates adaptive price bands +/// 2. Measures volatility from band distances +/// 3. Applies volatility normalization +/// 4. Uses JMA-style smoothing +/// 5. Provides multiple outputs +/// +/// Key characteristics: +/// - Adaptive measurement +/// - Noise reduction +/// - Multiple timeframe analysis +/// - Price band integration +/// - Volatility normalization +/// +/// Formula: +/// volty = max(|price - upperBand|, |price - lowerBand|) +/// bands = adaptive calculation using Jurik's methods +/// final = JMA smoothing of normalized volatility +/// +/// Market Applications: +/// - Dynamic position sizing +/// - Adaptive stop placement +/// - Volatility breakout systems +/// - Risk management +/// - Market regime detection +/// +/// Sources: +/// Mark Jurik Research +/// https://www.jurikresearch.com/ +/// +/// Note: Proprietary enhancement of volatility measurement +/// + public class Jvolty : AbstractBase { private readonly int _period; @@ -18,7 +55,6 @@ public class Jvolty : AbstractBase private double _prevMa1, _prevDet0, _prevDet1, _prevJma, _p_prevMa1, _p_prevDet0, _p_prevDet1, _p_prevJma; private double _vSum, _p_vSum; - public double UpperBand { get; set; } public double LowerBand { get; set; } public double Volty { get; set; } @@ -26,21 +62,15 @@ public class Jvolty : AbstractBase public double Jma { get; set; } public double AvgVolty { get; set; } - - /// - /// Initializes a new instance of the Jvolty class with the specified parameters. - /// - /// The period over which to calculate the Jvolty. - /// The phase parameter for the JMA-style calculation. - /// The short-term volatility period. - /// - /// Thrown when period is less than 1. - /// + /// The number of periods for volatility calculation. + /// Phase parameter for JMA smoothing (default 0). + /// Thrown when period is less than 1. public Jvolty(int period, int phase = 0) { if (period < 1) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); + throw new ArgumentOutOfRangeException(nameof(period), + "Period must be greater than or equal to 1."); } _period = period; _phase = Math.Clamp((phase * 0.01) + 1.5, 0.5, 2.5); @@ -53,22 +83,15 @@ public class Jvolty : AbstractBase Name = $"JVOLTY({period})"; } - /// - /// Initializes a new instance of the Jvolty class with the specified source and parameters. - /// - /// The source object to subscribe to for bar updates. - /// The period over which to calculate the Jvolty. - /// The phase parameter for the JMA-style calculation. - /// The short-term volatility period. + /// The data source object that publishes updates. + /// The number of periods for volatility calculation. + /// Phase parameter for JMA smoothing (default 0). public Jvolty(object source, int period, int phase = 0) : this(period, phase) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new BarSignal(Sub)); } - /// - /// Initializes the Jvolty instance by setting up the initial state. - /// public override void Init() { base.Init(); @@ -80,10 +103,6 @@ public class Jvolty : AbstractBase _vsumBuff.Clear(); } - /// - /// Manages the state of the Jvolty instance based on whether a new bar is being processed. - /// - /// Indicates whether the current input is a new bar. protected override void ManageState(bool isNew) { if (isNew) @@ -109,12 +128,6 @@ public class Jvolty : AbstractBase } } - /// - /// Performs the Jvolty calculation for the current bar. - /// - /// - /// The calculated Jvolty value for the current bar. - /// protected override double Calculation() { ManageState(Input.IsNew); @@ -125,26 +138,29 @@ public class Jvolty : AbstractBase _upperBand = _lowerBand = price; } + // Calculate volatility from band distances double del1 = price - _upperBand; double del2 = price - _lowerBand; double volty = Math.Max(Math.Abs(del1), Math.Abs(del2)); + // Calculate moving averages of volatility _vsumBuff.Add(volty, Input.IsNew); _vSum += (_vsumBuff[^1] - _vsumBuff[0]) / 10; _avoltyBuff.Add(_vSum, Input.IsNew); double avgvolty = _avoltyBuff.Average(); + // Normalize and adjust volatility double rvolty = (avgvolty > 0) ? volty / avgvolty : 1; rvolty = Math.Min(Math.Max(rvolty, 1.0), Math.Pow(_len1, 1.0 / _pow1)); double pow2 = Math.Pow(rvolty, _pow1); double Kv = Math.Pow(_beta, Math.Sqrt(pow2)); + // Update adaptive bands _upperBand = (del1 >= 0) ? price : price - (Kv * del1); _lowerBand = (del2 <= 0) ? price : price - (Kv * del2); - - + // Apply JMA smoothing double alpha = Math.Pow(_beta, pow2); double ma1 = (1 - alpha) * Input.Value + alpha * _prevMa1; _prevMa1 = ma1; @@ -153,11 +169,12 @@ public class Jvolty : AbstractBase _prevDet0 = det0; double ma2 = ma1 + _phase * det0; - double det1 = ((ma2 - _prevJma) * (1 - alpha) * (1 - alpha) ) + (alpha * alpha * _prevDet1); + double det1 = ((ma2 - _prevJma) * (1 - alpha) * (1 - alpha)) + (alpha * alpha * _prevDet1); _prevDet1 = det1; double jma = _prevJma + det1; _prevJma = jma; + // Update public properties UpperBand = _upperBand; LowerBand = _lowerBand; Volty = volty; @@ -169,5 +186,3 @@ public class Jvolty : AbstractBase return volty; } } - - diff --git a/lib/volatility/Rv.cs b/lib/volatility/Rv.cs index 4aec3783..b171a173 100644 --- a/lib/volatility/Rv.cs +++ b/lib/volatility/Rv.cs @@ -1,14 +1,48 @@ +using System; namespace QuanTAlib; /// -/// Represents a realized volatility calculator that measures the actual price fluctuations -/// observed in the market over a specific period. +/// RV: Realized Volatility +/// A precise volatility measure that captures actual observed price fluctuations +/// using high-frequency returns. RV provides a more accurate assessment of true +/// market volatility compared to traditional estimators. /// /// -/// The Realized class calculates volatility based on logarithmic returns. It can provide -/// both annualized and non-annualized volatility measures. The calculation uses a rolling -/// sum of squared returns for efficiency and assumes 252 trading days in a year for annualization. +/// The RV calculation process: +/// 1. Computes log returns +/// 2. Squares each return +/// 3. Maintains rolling sum +/// 4. Takes square root of average +/// 5. Optionally annualizes +/// +/// Key characteristics: +/// - Model-free measurement +/// - High-frequency capable +/// - Rolling calculation +/// - Memory efficient +/// - Optional annualization +/// +/// Formula: +/// RV = √(Σ(ln(P[t]/P[t-1]))²/n) * √252 +/// where: +/// P = price +/// n = number of observations +/// 252 = trading days per year +/// +/// Market Applications: +/// - High-frequency trading +/// - Options pricing +/// - Risk forecasting +/// - Market microstructure +/// - Volatility trading +/// +/// Sources: +/// Andersen, Bollerslev - "Answering the Skeptics" +/// https://en.wikipedia.org/wiki/Realized_volatility +/// +/// Note: Efficient implementation using rolling sums /// + public class Rv : AbstractBase { private readonly int Period; @@ -17,43 +51,33 @@ public class Rv : AbstractBase private double _previousClose; private double _sumSquaredReturns; - /// - /// Initializes a new instance of the Realized class with the specified period and annualization flag. - /// - /// The period over which to calculate realized volatility. - /// Whether to annualize the volatility (default is true). - /// - /// Thrown when period is less than 2. - /// + /// The number of periods for volatility calculation. + /// Whether to annualize the result (default true). + /// Thrown when period is less than 2. public Rv(int period, bool isAnnualized = true) { if (period < 2) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2."); + throw new ArgumentOutOfRangeException(nameof(period), + "Period must be greater than or equal to 2."); } Period = period; IsAnnualized = isAnnualized; - WarmupPeriod = period + 1; // We need one extra data point to calculate the first return + WarmupPeriod = period + 1; // Need extra point for first return _returns = new CircularBuffer(period); Name = $"Realized(period={period}, annualized={isAnnualized})"; Init(); } - /// - /// Initializes a new instance of the Realized class with a data source. - /// - /// The source object that publishes data. - /// The period over which to calculate realized volatility. - /// Whether to annualize the volatility (default is true). + /// The data source object that publishes updates. + /// The number of periods for volatility calculation. + /// Whether to annualize the result (default true). public Rv(object source, int period, bool isAnnualized = true) : this(period, isAnnualized) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - /// - /// Initializes the Realized instance by clearing buffers and resetting calculation variables. - /// public override void Init() { base.Init(); @@ -62,10 +86,6 @@ public class Rv : AbstractBase _sumSquaredReturns = 0; } - /// - /// Manages the state of the Realized instance based on whether a new value is being processed. - /// - /// Indicates whether the current input is a new value. protected override void ManageState(bool isNew) { if (isNew) @@ -75,21 +95,6 @@ public class Rv : AbstractBase } } - /// - /// Performs the realized volatility calculation for the current period. - /// - /// - /// The calculated realized volatility value for the current period. - /// - /// - /// This method calculates the volatility using the following steps: - /// 1. Compute logarithmic returns. - /// 2. Maintain a rolling sum of squared returns. - /// 3. Calculate the variance using the sum of squared returns. - /// 4. Take the square root of the variance to get volatility. - /// 5. If annualized, multiply by the square root of 252 (assumed trading days in a year). - /// The method returns 0 until enough data points are available for the calculation. - /// protected override double Calculation() { ManageState(Input.IsNew); @@ -97,26 +102,28 @@ public class Rv : AbstractBase double volatility = 0; if (_previousClose != 0) { + // Calculate log return double logReturn = Math.Log(Input.Value / _previousClose); if (_returns.Count == Period) { - // Remove the oldest squared return from the sum + // Maintain rolling sum by removing oldest squared return _sumSquaredReturns -= Math.Pow(_returns[0], 2); } + // Add new return and update sum _returns.Add(logReturn, Input.IsNew); _sumSquaredReturns += Math.Pow(logReturn, 2); if (_returns.Count == Period) { + // Calculate realized volatility double variance = _sumSquaredReturns / Period; volatility = Math.Sqrt(variance); if (IsAnnualized) { - // Assuming 252 trading days in a year. Adjust as needed. - volatility *= Math.Sqrt(252); + volatility *= Math.Sqrt(252); // Annualize using trading days } } } diff --git a/lib/volatility/Rvi.cs b/lib/volatility/Rvi.cs index 7047176c..f6468816 100644 --- a/lib/volatility/Rvi.cs +++ b/lib/volatility/Rvi.cs @@ -1,36 +1,61 @@ +using System; namespace QuanTAlib; /// -/// Represents a Relative Volatility Index (RVI) calculator, which measures the direction -/// of volatility in relation to price movements. +/// RVI: Relative Volatility Index +/// A technical indicator developed by Donald Dorsey that measures the direction +/// of volatility by comparing upward and downward price movements. RVI helps +/// identify whether volatility is increasing more in up or down moves. /// /// -/// The RVI was introduced by Donald Dorsey in the 1993 issue of Technical Analysis -/// of Stocks & Commodities Magazine. It focuses on the direction of price movements -/// in relation to volatility. The indicator uses standard deviation calculations -/// to determine whether volatility is increasing more in up moves or down moves. +/// The RVI calculation process: +/// 1. Separates price changes into up/down moves +/// 2. Calculates standard deviation for each +/// 3. Applies moving average smoothing +/// 4. Computes relative strength ratio +/// 5. Scales to percentage (0-100) /// -/// This implementation uses a combination of Standard Deviation and Simple Moving Average -/// calculations to compute the RVI. +/// Key characteristics: +/// - Oscillator (0-100 range) +/// - Directional volatility measure +/// - Combines volatility and momentum +/// - Uses standard deviation +/// - Smoothed output +/// +/// Formula: +/// RVI = 100 * SMA(StdDev(upMoves)) / (SMA(StdDev(upMoves)) + SMA(StdDev(downMoves))) +/// where: +/// upMove = max(close - prevClose, 0) +/// downMove = max(prevClose - close, 0) +/// +/// Market Applications: +/// - Trend confirmation +/// - Divergence analysis +/// - Volatility breakouts +/// - Market reversals +/// - Overbought/oversold levels +/// +/// Sources: +/// Donald Dorsey - "Technical Analysis of Stocks & Commodities" (1993) +/// https://www.investopedia.com/terms/r/relative_volatility_index.asp +/// +/// Note: Similar concept to RSI but using volatility /// + public class Rvi : AbstractBase { private readonly Stddev _upStdDev, _downStdDev; private readonly Sma _upSma, _downSma; private double _previousClose; - /// - /// Initializes a new instance of the Rvi class with the specified period. - /// - /// The period over which to calculate the RVI. - /// - /// Thrown when period is less than 2. - /// + /// The number of periods for RVI calculation. + /// Thrown when period is less than 2. public Rvi(int period) { if (period < 2) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2."); + throw new ArgumentOutOfRangeException(nameof(period), + "Period must be greater than or equal to 2."); } int Period = period; WarmupPeriod = period; @@ -42,30 +67,20 @@ public class Rvi : AbstractBase Init(); } - /// - /// Initializes a new instance of the Rvi class with the specified source and period. - /// - /// The source object to subscribe to for value updates. - /// The period over which to calculate the RVI. + /// The data source object that publishes updates. + /// The number of periods for RVI calculation. public Rvi(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - /// - /// Initializes the Rvi instance by setting up the initial state. - /// public override void Init() { base.Init(); _previousClose = 0; } - /// - /// Manages the state of the Rvi instance based on whether a new value is being processed. - /// - /// Indicates whether the current input is a new value. protected override void ManageState(bool isNew) { if (isNew) @@ -75,21 +90,6 @@ public class Rvi : AbstractBase } } - /// - /// Performs the RVI calculation for the current input. - /// - /// - /// The calculated RVI value for the current input. - /// - /// - /// This method calculates the RVI using the following steps: - /// 1. Calculate the change in price from the previous close. - /// 2. Determine the up move and down move based on the change. - /// 3. Calculate standard deviations of up and down moves. - /// 4. Apply a simple moving average to the standard deviations. - /// 5. Compute the RVI as a percentage of up volatility to total volatility. - /// The method returns 0 if the sum of up and down volatility is zero. - /// protected override double Calculation() { ManageState(Input.IsNew); @@ -97,14 +97,19 @@ public class Rvi : AbstractBase double close = Input.Value; double change = close - _previousClose; + // Separate into up and down moves double upMove = Math.Max(change, 0); double downMove = Math.Max(-change, 0); + // Calculate standard deviations and apply smoothing _upSma.Calc(_upStdDev.Calc(new TValue(Input.Time, upMove, Input.IsNew))); _downSma.Calc(_downStdDev.Calc(new TValue(Input.Time, downMove, Input.IsNew))); + // Calculate RVI ratio double rvi; - rvi = (_upSma.Value + _downSma.Value != 0) ? 100 * _upSma.Value / (_upSma.Value + _downSma.Value) : 0; + rvi = (_upSma.Value + _downSma.Value != 0) + ? 100 * _upSma.Value / (_upSma.Value + _downSma.Value) + : 0; _previousClose = close; IsHot = _index >= WarmupPeriod; From 6c67a0cf319cd343e155a2fd1cde5d375d68d2d1 Mon Sep 17 00:00:00 2001 From: Miha Date: Sun, 27 Oct 2024 16:11:08 -0700 Subject: [PATCH 05/17] Class optimization --- Directory.Build.props | 2 +- lib/averages/Afirma.cs | 122 ++++++++++++---------- lib/averages/Convolution.cs | 48 ++++++--- lib/averages/Dema.cs | 55 +++++----- lib/averages/Dsma.cs | 50 ++++++--- lib/averages/Dwma.cs | 23 +++-- lib/averages/Ema.cs | 95 ++++++------------ lib/averages/Epma.cs | 41 ++++---- lib/averages/Frama.cs | 62 ++++++++---- lib/averages/Fwma.cs | 35 +++---- lib/averages/Gma.cs | 29 ++++-- lib/averages/Hma.cs | 39 ++++++-- lib/averages/Htit.cs | 85 +++++++++------- lib/averages/Hwma.cs | 47 ++++++--- lib/averages/Jma.cs | 89 +++++++++-------- lib/averages/Kama.cs | 67 ++++++++----- lib/averages/Ltma.cs | 46 ++++++--- lib/averages/Maaf.cs | 57 +++++++---- lib/averages/Mama.cs | 151 ++++++++++++++++------------ lib/averages/Mgdi.cs | 28 ++++-- lib/averages/Mma.cs | 48 +++++---- lib/averages/Pwma.cs | 38 +++++-- lib/averages/Qema.cs | 28 ++++-- lib/averages/Rema.cs | 31 ++++-- lib/averages/Rma.cs | 88 ++++++---------- lib/averages/Sinema.cs | 45 +++++---- lib/averages/Sma.cs | 16 ++- lib/averages/Smma.cs | 28 ++++-- lib/averages/T3.cs | 51 ++++++---- lib/averages/Tema.cs | 51 +++++++--- lib/averages/Trima.cs | 23 +++-- lib/averages/Vidya.cs | 58 ++++++----- lib/averages/Wma.cs | 22 ++-- lib/averages/Zlema.cs | 189 ++++++++++++++++++----------------- lib/core/AbstractBarBase.cs | 54 ++++++++-- lib/core/abstractBase.cs | 124 +++++++++++------------ lib/core/circularbuffer.cs | 71 +++++++------ lib/core/tbar.cs | 122 ++++++++++++++-------- lib/core/tvalue.cs | 103 +++++++++++++------ lib/errors/Huber.cs | 46 +++++---- lib/errors/Mae.cs | 18 ++-- lib/errors/Mapd.cs | 29 ++++-- lib/errors/Mape.cs | 29 ++++-- lib/errors/Mase.cs | 17 +++- lib/errors/Mda.cs | 28 ++++-- lib/errors/Me.cs | 26 +++-- lib/errors/Mpe.cs | 29 ++++-- lib/errors/Mse.cs | 28 ++++-- lib/errors/Msle.cs | 32 ++++-- lib/errors/Rae.cs | 29 ++++-- lib/errors/Rmse.cs | 28 ++++-- lib/errors/Rmsle.cs | 32 ++++-- lib/errors/Rse.cs | 36 ++++--- lib/errors/Rsquared.cs | 39 +++++--- lib/errors/Smape.cs | 30 ++++-- lib/oscillators/Cmo.cs | 48 +++++---- lib/oscillators/Rsi.cs | 36 +++++-- lib/oscillators/Rsx.cs | 48 ++++++--- lib/quantalib.csproj | 1 - lib/statistics/Curvature.cs | 66 ++++++++---- lib/statistics/Entropy.cs | 80 +++++++++------ lib/statistics/Kurtosis.cs | 82 ++++++++++----- lib/statistics/Max.cs | 44 ++++++-- lib/statistics/Median.cs | 64 ++++++++++-- lib/statistics/Min.cs | 44 ++++++-- lib/statistics/Mode.cs | 80 +++++++++++---- lib/statistics/Percentile.cs | 90 ++++++++++++----- lib/statistics/Skew.cs | 86 ++++++++++------ lib/statistics/Slope.cs | 73 +++++++++----- lib/statistics/Stddev.cs | 46 +++++++-- lib/statistics/Variance.cs | 46 +++++++-- lib/statistics/Zscore.cs | 54 +++++++--- lib/volatility/Atr.cs | 28 ++++-- lib/volatility/Hv.cs | 57 ++++++++--- lib/volatility/Jvolty.cs | 89 +++++++++++------ lib/volatility/Rv.cs | 48 ++++++--- lib/volatility/Rvi.cs | 42 +++++--- 77 files changed, 2634 insertions(+), 1455 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 311a99e7..a5cfd53c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -11,7 +11,7 @@ false true AnyCPU - False + True bin\$(Configuration)\ False full diff --git a/lib/averages/Afirma.cs b/lib/averages/Afirma.cs index e80f2caf..0d7f1aa2 100644 --- a/lib/averages/Afirma.cs +++ b/lib/averages/Afirma.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -31,6 +31,9 @@ public class Afirma : AbstractBase private readonly double[] _armaBuffer; private readonly int _n; private readonly double _sx2, _sx3, _sx4, _sx5, _sx6, _den; + private readonly double _twoPi = 2.0 * Math.PI; + private readonly double _fourPi = 4.0 * Math.PI; + private readonly double _sixPi = 6.0 * Math.PI; /// The number of periods for the sinc filter calculation. /// The number of filter taps (filter length). Must be odd number. @@ -56,7 +59,7 @@ public class Afirma : AbstractBase _armaBuffer = new double[taps]; _n = (Taps - 1) / 2; - // Calculate least squares coefficients in the constructor + // Precalculate least squares coefficients _sx2 = (2 * _n + 1) / 3.0; _sx3 = _n * (_n + 1) / 2.0; _sx4 = _sx2 * (3 * _n * _n + 3 * _n - 1) / 5.0; @@ -78,6 +81,7 @@ public class Afirma : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -87,6 +91,34 @@ public class Afirma : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double CalculateSincWeight(double x) + { + return Math.Abs(x) < 1e-10 ? 1.0 : Math.Sin(x) / x; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double GetWindowWeight(int k, int tapsMinusOne) + { + switch (Window) + { + case WindowType.Rectangular: + return 1.0; + case WindowType.Hanning1: + return 0.50 - 0.50 * Math.Cos(_twoPi * k / tapsMinusOne); + case WindowType.Hanning2: + return 0.54 - 0.46 * Math.Cos(_twoPi * k / tapsMinusOne); + case WindowType.Blackman: + return 0.42 - 0.50 * Math.Cos(_twoPi * k / tapsMinusOne) + 0.08 * Math.Cos(_fourPi * k / tapsMinusOne); + case WindowType.BlackmanHarris: + return 0.35875 - 0.48829 * Math.Cos(_twoPi * k / tapsMinusOne) + + 0.14128 * Math.Cos(_fourPi * k / tapsMinusOne) - + 0.01168 * Math.Cos(_sixPi * k / tapsMinusOne); + default: + return 1.0; + } + } + protected override double Calculation() { ManageState(IsNew); @@ -94,71 +126,59 @@ public class Afirma : AbstractBase if (_index >= Taps) { - double a0 = _buffer[_n]; - double a1 = _buffer[_n] - _buffer[_n + 1]; - double sx2y = 0.0; - double sx3y = 0.0; - - for (int i = 0; i <= _n; i++) - { - sx2y += i * i * _buffer[_n - i]; - sx3y += i * i * i * _buffer[_n - i]; - } - - sx2y = 2.0 * sx2y / _n / (_n + 1); - sx3y = 2.0 * sx3y / _n / (_n + 1); - double p = sx2y - a0 * _sx2 - a1 * _sx3; - double q = sx3y - a0 * _sx3 - a1 * _sx4; - double a2 = (p * _sx6 / _sx5 - q) / _den; - double a3 = (q * _sx4 / _sx5 - p) / _den; - - for (int k = 0; k <= _n; k++) - { - _armaBuffer[_n - k] = a0 + k * a1 + k * k * a2 + k * k * k * a3; - } + CalculateAdaptiveCoefficients(); } double result = 0.0; for (int k = 0; k < Taps; k++) { - result += _buffer[k] * _weights[k] / _wsum; + result += _buffer[k] * _weights[k]; } IsHot = _index >= WarmupPeriod; - return result; + return result / _wsum; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CalculateAdaptiveCoefficients() + { + double a0 = _buffer[_n]; + double a1 = _buffer[_n] - _buffer[_n + 1]; + double sx2y = 0.0; + double sx3y = 0.0; + + for (int i = 0; i <= _n; i++) + { + double i2 = i * i; + sx2y += i2 * _buffer[_n - i]; + sx3y += i2 * i * _buffer[_n - i]; + } + + sx2y = 2.0 * sx2y / _n / (_n + 1); + sx3y = 2.0 * sx3y / _n / (_n + 1); + double p = sx2y - a0 * _sx2 - a1 * _sx3; + double q = sx3y - a0 * _sx3 - a1 * _sx4; + double a2 = (p * _sx6 / _sx5 - q) / _den; + double a3 = (q * _sx4 / _sx5 - p) / _den; + + for (int k = 0; k <= _n; k++) + { + double k2 = k * k; + _armaBuffer[_n - k] = a0 + k * a1 + k2 * a2 + k2 * k * a3; + } } private double CalculateWeights() { double wsum = 0.0; double centerTap = (Taps - 1) / 2.0; + int tapsMinusOne = Taps - 1; + for (int k = 0; k < Taps; k++) { - double windowWeight; - switch (Window) - { - case WindowType.Rectangular: - windowWeight = 1.0; - break; - case WindowType.Hanning1: - windowWeight = 0.50 - 0.50 * Math.Cos(2.0 * Math.PI * k / (Taps - 1)); - break; - case WindowType.Hanning2: - windowWeight = 0.54 - 0.46 * Math.Cos(2.0 * Math.PI * k / (Taps - 1)); - break; - case WindowType.Blackman: - windowWeight = 0.42 - 0.50 * Math.Cos(2.0 * Math.PI * k / (Taps - 1)) + 0.08 * Math.Cos(4.0 * Math.PI * k / (Taps - 1)); - break; - case WindowType.BlackmanHarris: - windowWeight = 0.35875 - 0.48829 * Math.Cos(2.0 * Math.PI * k / (Taps - 1)) + 0.14128 * Math.Cos(4.0 * Math.PI * k / (Taps - 1)) - 0.01168 * Math.Cos(6.0 * Math.PI * k / (Taps - 1)); - break; - default: - windowWeight = 1.0; - break; - } - - double sincWeight; - sincWeight = Math.Abs(k - centerTap) < 1e-10 ? 1.0 : Math.Sin(Math.PI * (k - centerTap) / Periods) / (Math.PI * (k - centerTap) / Periods); + double windowWeight = GetWindowWeight(k, tapsMinusOne); + double x = Math.PI * (k - centerTap) / Periods; + double sincWeight = CalculateSincWeight(x); _weights[k] = windowWeight * sincWeight; wsum += _weights[k]; diff --git a/lib/averages/Convolution.cs b/lib/averages/Convolution.cs index 4a7a75fc..08162bc8 100644 --- a/lib/averages/Convolution.cs +++ b/lib/averages/Convolution.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -17,6 +17,7 @@ public class Convolution : AbstractBase private readonly int _kernelSize; private readonly CircularBuffer _buffer; private readonly double[] _normalizedKernel; + private int _activeLength; /// Array of weights defining the convolution operation. The length of this array determines the filter's window size. /// Thrown when kernel is null or empty. @@ -41,22 +42,27 @@ public class Convolution : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private new void Init() { base.Init(); _buffer.Clear(); - Array.Copy(_kernel, _normalizedKernel, _kernelSize); + System.Array.Copy(_kernel, _normalizedKernel, _kernelSize); + _activeLength = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) { _lastValidValue = Input.Value; _index++; + _activeLength = System.Math.Min(_index, _kernelSize); } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override double GetLastValid() { return _lastValidValue; @@ -65,7 +71,6 @@ public class Convolution : AbstractBase protected override double Calculation() { ManageState(Input.IsNew); - _buffer.Add(Input.Value, Input.IsNew); // Normalize kernel on each calculation until buffer is full @@ -80,37 +85,56 @@ public class Convolution : AbstractBase return result; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void NormalizeKernel() { - int activeLength = Math.Min(_index, _kernelSize); double sum = 0; // Calculate the sum of the active kernel elements - for (int i = 0; i < activeLength; i++) + for (int i = 0; i < _activeLength; i++) { sum += _kernel[i]; } // Normalize the kernel or set equal weights if the sum is zero - double normalizationFactor = (sum != 0) ? sum : activeLength; - for (int i = 0; i < activeLength; i++) + double normalizationFactor = (sum != 0) ? sum : _activeLength; + double invNormFactor = 1.0 / normalizationFactor; + + for (int i = 0; i < _activeLength; i++) { - _normalizedKernel[i] = _kernel[i] / normalizationFactor; + _normalizedKernel[i] = _kernel[i] * invNormFactor; } // Set the rest of the normalized kernel to zero - Array.Clear(_normalizedKernel, activeLength, _kernelSize - activeLength); + if (_activeLength < _kernelSize) + { + System.Array.Clear(_normalizedKernel, _activeLength, _kernelSize - _activeLength); + } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private double ConvolveBuffer() { double sum = 0; var bufferSpan = _buffer.GetSpan(); - int activeLength = Math.Min(_index, _kernelSize); + int offset = _activeLength - 1; - for (int i = 0; i < activeLength; i++) + // Unroll the loop for better performance when possible + int i = 0; + while (i <= offset - 3) { - sum += bufferSpan[activeLength - 1 - i] * _normalizedKernel[i]; + sum += bufferSpan[offset - i] * _normalizedKernel[i] + + bufferSpan[offset - (i + 1)] * _normalizedKernel[i + 1] + + bufferSpan[offset - (i + 2)] * _normalizedKernel[i + 2] + + bufferSpan[offset - (i + 3)] * _normalizedKernel[i + 3]; + i += 4; + } + + // Handle remaining elements + while (i < _activeLength) + { + sum += bufferSpan[offset - i] * _normalizedKernel[i]; + i++; } return sum; diff --git a/lib/averages/Dema.cs b/lib/averages/Dema.cs index d3e37bfb..d0673ae4 100644 --- a/lib/averages/Dema.cs +++ b/lib/averages/Dema.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -7,12 +8,8 @@ namespace QuanTAlib; /// smoothness, at the cost of overshooting the signal line. /// /// -/// Smoothness: ★★★☆☆ (3/5) -/// Sensitivity: ★★★★☆ (4/5) -/// Overshooting: ★★★☆☆ (3/5) -/// Lag: ★★★★☆ (4/5) -/// /// Sources: +/// https://en.wikipedia.org/wiki/Double_exponential_moving_average /// https://www.investopedia.com/terms/d/double-exponential-moving-average.asp /// https://www.tradingview.com/support/solutions/43000502589-double-exponential-moving-average-dema/ /// @@ -21,12 +18,12 @@ namespace QuanTAlib; /// public class Dema : AbstractBase { - // inherited _index - // inherited _value private readonly int _period; + private readonly double _k; + private readonly double _epsilon = 1e-10; private double _lastEma1, _p_lastEma1; private double _lastEma2, _p_lastEma2; - private double _k, _e, _p_e; + private double _e, _p_e; public Dema(int period) { @@ -35,9 +32,10 @@ public class Dema : AbstractBase throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); } _period = period; + _k = 2.0 / (_period + 1); Name = "Dema"; double percentile = 0.85; //targeting 85th percentile of correctness of converging EMA - WarmupPeriod = (int)Math.Ceiling(-period * Math.Log(1 - percentile)); + WarmupPeriod = (int)System.Math.Ceiling(-period * System.Math.Log(1 - percentile)); Init(); } @@ -46,17 +44,17 @@ public class Dema : AbstractBase var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - //inhereted public void Sub(object source, in ValueEventArgs args) + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); - _k = 2.0 / (_period + 1); _e = 1.0; _lastEma1 = 0; _lastEma2 = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -74,30 +72,31 @@ public class Dema : AbstractBase } } - /// - /// Core DEMA calculation - /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateEma(double input, double lastEma) + { + return _k * (input - lastEma) + lastEma; + } + protected override double Calculation() { ManageState(Input.IsNew); - double result, _ema1, _ema2; - // compensator for early ema values - _e = (_e > 1e-10) ? (1 - _k) * _e : 0; - double _invE = (_e > 1e-10) ? 1 / (1 - _e) : 1; + // Compensator for early EMA values + _e = (_e > _epsilon) ? (1 - _k) * _e : 0; + double invE = (_e > _epsilon) ? 1 / (1 - _e) : 1; - // Calculate EMA1 - _ema1 = _k * (Input.Value - _lastEma1) + _lastEma1; + // Calculate EMAs + double ema1 = CalculateEma(Input.Value, _lastEma1); + double compensatedEma1 = ema1 * invE; + double ema2 = CalculateEma(compensatedEma1, _lastEma2); - // Calculate EMA2 using compensatedEma1 - _ema2 = _k * (_ema1 * _invE - _lastEma2) + _lastEma2; + // Store values for next iteration + _lastEma1 = ema1; + _lastEma2 = ema2; - // Calculate DEMA - double _dema = 2 * _ema1 * _invE - (_ema2 * _invE); - - result = _dema; - _lastEma1 = _ema1; - _lastEma2 = _ema2; + // Calculate final DEMA + double result = 2 * compensatedEma1 - (ema2 * invE); IsHot = _index >= WarmupPeriod; return result; diff --git a/lib/averages/Dsma.cs b/lib/averages/Dsma.cs index 5fb0c9ba..70f80191 100644 --- a/lib/averages/Dsma.cs +++ b/lib/averages/Dsma.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -25,6 +26,10 @@ public class Dsma : AbstractBase private readonly CircularBuffer _buffer; private readonly double _c1, _c2, _c3; private readonly double _scaleFactor; + private readonly double _periodRecip; // 1/_period + private readonly double _scaleByPeriod; // 5/_period + private readonly double _c1Half; // _c1/2 + private double _lastDsma, _p_lastDsma; private double _filt, _filt1, _filt2, _zeros, _zeros1; private double _p_filt, _p_filt1, _p_filt2, _p_zeros, _p_zeros1; @@ -46,16 +51,20 @@ public class Dsma : AbstractBase throw new ArgumentOutOfRangeException(nameof(scaleFactor), "Scale factor must be between 0 and 1 (exclusive)."); } _period = period; + _periodRecip = 1.0 / period; _scaleFactor = scaleFactor; _buffer = new CircularBuffer(period); // SuperSmoother filter coefficients - double _a1 = Math.Exp(-1.414 * Math.PI / (0.5 * period)); - double _b1 = 2 * _a1 * Math.Cos(1.414 * Math.PI / (0.5 * period)); + double halfPeriod = 0.5 * period; + double a1 = System.Math.Exp(-1.414 * System.Math.PI / halfPeriod); + double b1 = 2.0 * a1 * System.Math.Cos(1.414 * System.Math.PI / halfPeriod); - _c2 = _b1; - _c3 = -_a1 * _a1; - _c1 = 1 - _c2 - _c3; + _c2 = b1; + _c3 = -a1 * a1; + _c1 = 1.0 - _c2 - _c3; + _c1Half = _c1 * 0.5; + _scaleByPeriod = 5.0 / period; Name = "Dsma"; WarmupPeriod = (int)(period * 1.5); // A conservative estimate @@ -68,6 +77,7 @@ public class Dsma : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -77,6 +87,7 @@ public class Dsma : AbstractBase _isInit = false; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -102,6 +113,19 @@ public class Dsma : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateSuperSmootherFilter() + { + return _c1Half * (_zeros + _zeros1) + _c2 * _filt1 + _c3 * _filt2; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateAdaptiveAlpha(double scaledFilt) + { + double alpha = _scaleFactor * System.Math.Abs(scaledFilt) * _scaleByPeriod; + return System.Math.Clamp(alpha, 0.1, 1.0); + } + protected override double Calculation() { ManageState(Input.IsNew); @@ -117,20 +141,18 @@ public class Dsma : AbstractBase _zeros = Input.Value - _lastDsma; // SuperSmoother Filter - _filt = _c1 * (_zeros + _zeros1) / 2 + _c2 * _filt1 + _c3 * _filt2; + _filt = CalculateSuperSmootherFilter(); // Update buffer for RMS calculation - _buffer.Add(_filt * _filt, Input.IsNew); + double filtSquared = _filt * _filt; + _buffer.Add(filtSquared, Input.IsNew); // Compute RMS (Root Mean Square) - double rms = Math.Sqrt(_buffer.Sum() / _period); + double rms = System.Math.Sqrt(_buffer.Sum() * _periodRecip); - // Rescale Filt in terms of Standard Deviations - double scaledFilt = rms != 0 ? _filt / rms : 0; - - // Calculate adaptive alpha - double alpha = _scaleFactor * Math.Abs(scaledFilt) * 5 / _period; - alpha = Math.Max(0.1, Math.Min(1.0, alpha)); + // Rescale Filt in terms of Standard Deviations and calculate adaptive alpha + double scaledFilt = rms > 0 ? _filt / rms : 0; + double alpha = CalculateAdaptiveAlpha(scaledFilt); // DSMA calculation double dsma = alpha * Input.Value + (1 - alpha) * _lastDsma; diff --git a/lib/averages/Dwma.cs b/lib/averages/Dwma.cs index ba09d84c..88c87c7b 100644 --- a/lib/averages/Dwma.cs +++ b/lib/averages/Dwma.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -25,16 +26,18 @@ public class Dwma : AbstractBase { private readonly Wma _innerWma; private readonly Wma _outerWma; + private readonly int _period; public Dwma(int period) { if (period < 1) { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } + _period = period; _innerWma = new Wma(period); _outerWma = new Wma(period); - Name = "Wma"; + Name = "Dwma"; WarmupPeriod = 2 * period - 1; Init(); } @@ -45,6 +48,7 @@ public class Dwma : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -52,6 +56,7 @@ public class Dwma : AbstractBase _outerWma.Init(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -61,19 +66,23 @@ public class Dwma : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override double GetLastValid() + { + return _lastValidValue; + } + protected override double Calculation() { ManageState(Input.IsNew); // Calculate inner WMA - TValue innerResult = _innerWma.Calc(Input); + var innerResult = _innerWma.Calc(Input); // Calculate outer WMA using the result of inner WMA - TValue outerResult = _outerWma.Calc(innerResult); + var outerResult = _outerWma.Calc(innerResult); - double result = outerResult.Value; IsHot = _index >= WarmupPeriod; - - return result; + return outerResult.Value; } } diff --git a/lib/averages/Ema.cs b/lib/averages/Ema.cs index 3e38f745..7ebd8014 100644 --- a/lib/averages/Ema.cs +++ b/lib/averages/Ema.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -22,43 +23,14 @@ namespace QuanTAlib; /// public class Ema : AbstractBase { - // inherited _index - // inherited _value - - /// - /// The period for the EMA calculation. - /// private readonly int _period; - - /// - /// Circular buffer for SMA calculation. - /// - private CircularBuffer _sma; - - /// - /// The last calculated EMA value. - /// - private double _lastEma, _p_lastEma; - - /// - /// Compensator for early EMA values. - /// - private double _e, _p_e; - - /// - /// The smoothing factor for EMA calculation. - /// private readonly double _k; - - /// - /// Flags to track initialization status. - /// - private bool _isInit, _p_isInit; - - /// - /// Flag to determine whether to use SMA for initial values. - /// private readonly bool _useSma; + private readonly double _epsilon = 1e-10; + private CircularBuffer _sma; + private double _lastEma, _p_lastEma; + private double _e, _p_e; + private bool _isInit, _p_isInit; /// /// Initializes a new instance of the Ema class with a specified period. @@ -70,14 +42,14 @@ public class Ema : AbstractBase { if (period < 1) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); + throw new System.ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); } _period = period; _k = 2.0 / (_period + 1); _useSma = useSma; - _sma = new(period); + _sma = new(_period); Name = "Ema"; - WarmupPeriod = (int)Math.Ceiling(Math.Log(0.05) / Math.Log(1 - _k)); //95th percentile + WarmupPeriod = (int)System.Math.Ceiling(System.Math.Log(0.05) / System.Math.Log(1 - _k)); //95th percentile Init(); } @@ -92,7 +64,7 @@ public class Ema : AbstractBase _sma = new(1); Name = "Ema"; _period = 1; - WarmupPeriod = (int)Math.Ceiling(Math.Log(0.05) / Math.Log(1 - _k)); //95th percentile + WarmupPeriod = (int)System.Math.Ceiling(System.Math.Log(0.05) / System.Math.Log(1 - _k)); //95th percentile Init(); } @@ -108,9 +80,7 @@ public class Ema : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - /// - /// Initializes the Ema instance. - /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -121,10 +91,7 @@ public class Ema : AbstractBase _sma = new(_period); } - /// - /// Manages the state of the Ema instance. - /// - /// Indicates whether the input is new. + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -142,21 +109,27 @@ public class Ema : AbstractBase } } - /// - /// Performs the EMA calculation. - /// - /// The calculated EMA value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateEma(double input, double lastEma) + { + return _k * (input - lastEma) + lastEma; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CompensateEma(double ema) + { + return (_useSma || _e <= _epsilon) ? ema : ema / (1 - _e); + } + protected override double Calculation() { - double result, _ema; ManageState(Input.IsNew); - // when _UseSma == true, use SMA calculation until we have enough data points + double ema; if (!_isInit && _useSma) { _sma.Add(Input.Value, Input.IsNew); - _ema = _sma.Average(); - result = _ema; + ema = _sma.Average(); if (_index >= _period) { _isInit = true; @@ -164,16 +137,14 @@ public class Ema : AbstractBase } else { - // compensator for early ema values - _e = (_e > 1e-10) ? (1 - _k) * _e : 0; - - _ema = _k * (Input.Value - _lastEma) + _lastEma; - - // _useSma decides if we use compensator or not - result = (_useSma || _e <= double.Epsilon) ? _ema : _ema / (1 - _e); + // Compensator for early EMA values + _e = (_e > _epsilon) ? (1 - _k) * _e : 0; + ema = CalculateEma(Input.Value, _lastEma); + ema = CompensateEma(ema); } - _lastEma = _ema; + + _lastEma = ema; IsHot = _index >= WarmupPeriod; - return result; + return ema; } } diff --git a/lib/averages/Epma.cs b/lib/averages/Epma.cs index f9985e9b..170a0319 100644 --- a/lib/averages/Epma.cs +++ b/lib/averages/Epma.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -27,6 +27,7 @@ public class Epma : AbstractBase { private readonly int _period; private readonly Convolution _convolution; + private readonly double[] _baseKernel; /// The number of data points used in the EPMA calculation. /// Thrown when period is less than 1. @@ -34,10 +35,11 @@ public class Epma : AbstractBase { if (period < 1) { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } _period = period; - _convolution = new Convolution(GenerateKernel(_period)); + _baseKernel = GenerateKernel(_period); + _convolution = new Convolution(_baseKernel); Name = "Epma"; WarmupPeriod = period; Init(); @@ -51,12 +53,14 @@ public class Epma : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private new void Init() { base.Init(); _convolution.Init(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -66,24 +70,31 @@ public class Epma : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double CalculateKernelSum(int period) + { + // Using arithmetic sequence sum formula: n(a1 + an)/2 + // where a1 = (2p-1) and an = (2p-1) - 3(n-1) + double firstTerm = 2 * period - 1; + double lastTerm = firstTerm - 3 * (period - 1); + return period * (firstTerm + lastTerm) * 0.5; + } + protected override double Calculation() { ManageState(Input.IsNew); // Use Convolution for calculation - TValue convolutionResult = _convolution.Calc(Input); - + var convolutionResult = _convolution.Calc(Input); double result = convolutionResult.Value; // Adjust for partial periods during warmup if (_index < _period) { - double[] partialKernel = GenerateKernel(_index); - result /= partialKernel.Sum(); + result *= CalculateKernelSum(_period) / CalculateKernelSum(_index); } IsHot = _index >= WarmupPeriod; - return result; } @@ -92,21 +103,17 @@ public class Epma : AbstractBase /// /// The period for which to generate the kernel. /// An array of normalized weights for the convolution operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double[] GenerateKernel(int period) { double[] kernel = new double[period]; - double weightSum = 0; + double weightSum = CalculateKernelSum(period); + double invWeightSum = 1.0 / weightSum; + double baseWeight = 2 * period - 1; for (int i = 0; i < period; i++) { - kernel[i] = (2 * period - 1) - 3 * i; - weightSum += kernel[i]; - } - - // Normalize the kernel - for (int i = 0; i < period; i++) - { - kernel[i] /= weightSum; + kernel[i] = (baseWeight - 3 * i) * invWeightSum; } return kernel; diff --git a/lib/averages/Frama.cs b/lib/averages/Frama.cs index 9e509b82..aa4adaca 100644 --- a/lib/averages/Frama.cs +++ b/lib/averages/Frama.cs @@ -1,5 +1,4 @@ -using System; - +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -28,6 +27,11 @@ namespace QuanTAlib; public class Frama : AbstractBase { private readonly int _period; + private readonly int _halfPeriod; + private readonly double _periodRecip; + private readonly double _halfPeriodRecip; + private readonly double _log2 = System.Math.Log(2); + private readonly double _epsilon = double.Epsilon; private readonly CircularBuffer _buffer; private double _lastFrama; private double _prevLastFrama; @@ -37,9 +41,12 @@ public class Frama : AbstractBase public Frama(int period) { if (period < 2) - throw new ArgumentException("Period must be at least 2", nameof(period)); + throw new System.ArgumentException("Period must be at least 2", nameof(period)); _period = period; + _halfPeriod = period / 2; + _periodRecip = 1.0 / period; + _halfPeriodRecip = 1.0 / _halfPeriod; _buffer = new CircularBuffer(period); WarmupPeriod = period; } @@ -52,6 +59,7 @@ public class Frama : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -60,6 +68,7 @@ public class Frama : AbstractBase _prevLastFrama = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -73,6 +82,26 @@ public class Frama : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void UpdateMinMax(double price, ref double high, ref double low) + { + high = System.Math.Max(high, price); + low = System.Math.Min(low, price); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateAlpha(double dimension) + { + double alpha = System.Math.Exp(-4.6 * (dimension - 1)); + return System.Math.Clamp(alpha, 0.01, 1.0); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override double GetLastValid() + { + return _lastFrama; + } + protected override double Calculation() { ManageState(Input.IsNew); @@ -85,7 +114,6 @@ public class Frama : AbstractBase return _lastFrama; } - int half = _period / 2; double hh = double.MinValue, ll = double.MaxValue; double hh1 = double.MinValue, ll1 = double.MaxValue; double hh2 = double.MinValue, ll2 = double.MaxValue; @@ -93,37 +121,27 @@ public class Frama : AbstractBase for (int i = 0; i < _period; i++) { double price = _buffer[i]; - hh = Math.Max(hh, price); - ll = Math.Min(ll, price); + UpdateMinMax(price, ref hh, ref ll); - if (i < half) + if (i < _halfPeriod) { - hh1 = Math.Max(hh1, price); - ll1 = Math.Min(ll1, price); + UpdateMinMax(price, ref hh1, ref ll1); } else { - hh2 = Math.Max(hh2, price); - ll2 = Math.Min(ll2, price); + UpdateMinMax(price, ref hh2, ref ll2); } } - double n1 = (hh - ll) / _period; - double n2 = (hh1 - ll1 + hh2 - ll2) / (_period / 2); + double n1 = (hh - ll) * _periodRecip; + double n2 = (hh1 - ll1 + hh2 - ll2) * _halfPeriodRecip; - double d = (Math.Log(n2 + double.Epsilon) - Math.Log(n1 + double.Epsilon)) / Math.Log(2); - - double alpha = Math.Exp(-4.6 * (d - 1)); - alpha = Math.Max(Math.Min(alpha, 1), 0.01); // Ensure alpha is between 0.01 and 1 + double dimension = (System.Math.Log(n2 + _epsilon) - System.Math.Log(n1 + _epsilon)) / _log2; + double alpha = CalculateAlpha(dimension); _lastFrama = alpha * (Input.Value - _lastFrama) + _lastFrama; IsHot = _index >= WarmupPeriod; return _lastFrama; } - - protected override double GetLastValid() - { - return _lastFrama; - } } diff --git a/lib/averages/Fwma.cs b/lib/averages/Fwma.cs index 98ee1bb1..bff1bf4d 100644 --- a/lib/averages/Fwma.cs +++ b/lib/averages/Fwma.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -27,6 +27,7 @@ namespace QuanTAlib; public class Fwma : AbstractBase { private readonly Convolution _convolution; + private readonly double[] _kernel; /// The number of data points used in the FWMA calculation. /// Thrown when period is less than 1. @@ -34,9 +35,10 @@ public class Fwma : AbstractBase { if (period < 1) { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } - _convolution = new Convolution(GenerateKernel(period)); + _kernel = GenerateKernel(period); + _convolution = new Convolution(_kernel); Name = "Fwma"; WarmupPeriod = period; Init(); @@ -55,41 +57,42 @@ public class Fwma : AbstractBase /// /// The period for which to generate the kernel. /// An array of normalized Fibonacci-based weights for the convolution operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double[] GenerateKernel(int period) { double[] kernel = new double[period]; double[] fibSeries = new double[period]; - double weightSum = 0; - // Generate Fibonacci series + // Generate Fibonacci series with running sum fibSeries[0] = fibSeries[1] = 1; + double weightSum = 2.0; // Initial sum for first two Fibonacci numbers + for (int i = 2; i < period; i++) { fibSeries[i] = fibSeries[i - 1] + fibSeries[i - 2]; + weightSum += fibSeries[i]; } - // Reverse the series to give more weight to recent prices - for (int i = 0; i < period; i++) - { - kernel[i] = fibSeries[period - 1 - i]; - weightSum += kernel[i]; - } + // Calculate inverse of weight sum for normalization + double invWeightSum = 1.0 / weightSum; - // Normalize the kernel + // Reverse and normalize the series in one pass for (int i = 0; i < period; i++) { - kernel[i] /= weightSum; + kernel[i] = fibSeries[period - 1 - i] * invWeightSum; } return kernel; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private new void Init() { base.Init(); _convolution.Init(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -104,11 +107,9 @@ public class Fwma : AbstractBase ManageState(Input.IsNew); // Use Convolution for calculation - TValue convolutionResult = _convolution.Calc(Input); - - double result = convolutionResult.Value; + var convolutionResult = _convolution.Calc(Input); IsHot = _index >= WarmupPeriod; - return result; + return convolutionResult.Value; } } diff --git a/lib/averages/Gma.cs b/lib/averages/Gma.cs index 74fafc08..71d80a88 100644 --- a/lib/averages/Gma.cs +++ b/lib/averages/Gma.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -27,6 +27,7 @@ namespace QuanTAlib; public class Gma : AbstractBase { private readonly Convolution _convolution; + private readonly double[] _kernel; /// The number of data points used in the GMA calculation. /// Thrown when period is less than 1. @@ -34,9 +35,10 @@ public class Gma : AbstractBase { if (period < 1) { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } - _convolution = new Convolution(GenerateKernel(period)); + _kernel = GenerateKernel(period); + _convolution = new Convolution(_kernel); Name = "Gma"; WarmupPeriod = period; Init(); @@ -56,34 +58,41 @@ public class Gma : AbstractBase /// The period for which to generate the kernel. /// The standard deviation parameter controlling the spread of the Gaussian curve. Default is 1.0. /// An array of normalized Gaussian-based weights for the convolution operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double[] GenerateKernel(int period, double sigma = 1.0) { double[] kernel = new double[period]; double weightSum = 0; int center = period / 2; + double centerRecip = 1.0 / center; + double sigmaSquared2 = 2.0 * sigma * sigma; + // Calculate weights and sum in one pass for (int i = 0; i < period; i++) { - double x = (i - center) / (double)center; - kernel[i] = Math.Exp(-(x * x) / (2 * sigma * sigma)); + double x = (i - center) * centerRecip; + kernel[i] = System.Math.Exp(-(x * x) / sigmaSquared2); weightSum += kernel[i]; } - // Normalize the kernel + // Normalize using multiplication instead of division + double invWeightSum = 1.0 / weightSum; for (int i = 0; i < period; i++) { - kernel[i] /= weightSum; + kernel[i] *= invWeightSum; } return kernel; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private new void Init() { base.Init(); _convolution.Init(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -98,11 +107,9 @@ public class Gma : AbstractBase ManageState(Input.IsNew); // Use Convolution for calculation - TValue convolutionResult = _convolution.Calc(Input); - - double result = convolutionResult.Value; + var convolutionResult = _convolution.Calc(Input); IsHot = _index >= WarmupPeriod; - return result; + return convolutionResult.Value; } } diff --git a/lib/averages/Hma.cs b/lib/averages/Hma.cs index 5b78c796..20e4b833 100644 --- a/lib/averages/Hma.cs +++ b/lib/averages/Hma.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -29,6 +29,11 @@ namespace QuanTAlib; public class Hma : AbstractBase { private readonly Convolution _wmaHalf, _wmaFull, _wmaFinal; + private readonly int _period; + private readonly int _sqrtPeriod; + private readonly double[] _kernelHalf; + private readonly double[] _kernelFull; + private readonly double[] _kernelFinal; /// The number of data points used in the HMA calculation. Must be at least 2. /// Thrown when period is less than 2. @@ -36,12 +41,21 @@ public class Hma : AbstractBase { if (period < 2) { - throw new ArgumentException("Period must be greater than or equal to 2.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 2.", nameof(period)); } - int _sqrtPeriod = (int)Math.Sqrt(period); - _wmaHalf = new Convolution(GenerateWmaKernel(period / 2)); - _wmaFull = new Convolution(GenerateWmaKernel(period)); - _wmaFinal = new Convolution(GenerateWmaKernel(_sqrtPeriod)); + _period = period; + _sqrtPeriod = (int)System.Math.Sqrt(period); + + // Generate all kernels once + _kernelHalf = GenerateWmaKernel(period / 2); + _kernelFull = GenerateWmaKernel(period); + _kernelFinal = GenerateWmaKernel(_sqrtPeriod); + + // Initialize convolutions with pre-generated kernels + _wmaHalf = new Convolution(_kernelHalf); + _wmaFull = new Convolution(_kernelFull); + _wmaFinal = new Convolution(_kernelFinal); + Name = "Hma"; WarmupPeriod = period + _sqrtPeriod - 1; Init(); @@ -60,19 +74,22 @@ public class Hma : AbstractBase /// /// The period for which to generate the kernel. /// An array of linearly weighted values for the convolution operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double[] GenerateWmaKernel(int period) { double[] kernel = new double[period]; - double weightSum = period * (period + 1) / 2.0; + double weightSum = period * (period + 1) * 0.5; // Multiply by 0.5 instead of dividing by 2 + double invWeightSum = 1.0 / weightSum; for (int i = 0; i < period; i++) { - kernel[i] = (period - i) / weightSum; + kernel[i] = (period - i) * invWeightSum; } return kernel; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private new void Init() { base.Init(); @@ -81,6 +98,7 @@ public class Hma : AbstractBase _wmaFinal.Init(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -99,10 +117,11 @@ public class Hma : AbstractBase double wmaFullResult = _wmaFull.Calc(Input).Value; // Calculate 2*WMA(n/2) - WMA(n) - double intermediateResult = 2 * wmaHalfResult - wmaFullResult; + double intermediateResult = 2.0 * wmaHalfResult - wmaFullResult; // Calculate final WMA - double result = _wmaFinal.Calc(new TValue(Input.Time, intermediateResult, Input.IsNew)).Value; + var finalInput = new TValue(Input.Time, intermediateResult, Input.IsNew); + double result = _wmaFinal.Calc(finalInput).Value; IsHot = _index >= WarmupPeriod; return result; diff --git a/lib/averages/Htit.cs b/lib/averages/Htit.cs index 0c911f88..ea6dd6eb 100644 --- a/lib/averages/Htit.cs +++ b/lib/averages/Htit.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -42,28 +42,30 @@ public class Htit : AbstractBase private readonly CircularBuffer _sdBuffer = new(2); private readonly CircularBuffer _itBuffer = new(4); + private const double ALPHA = 0.2; + private const double BETA = 0.8; + private const double TWO_PI = 2.0 * System.Math.PI; + private const double MIN_PERIOD = 6.0; + private const double MAX_PERIOD = 50.0; + private const double PERIOD_UPPER_LIMIT = 1.5; + private const double PERIOD_LOWER_LIMIT = 0.67; + private double _lastPd = 0; private double _p_lastPd = 0; - /// - /// Initializes a new instance of the Htit class. - /// public Htit() { Name = "Htit"; WarmupPeriod = 12; } - /// - /// Initializes a new instance of the Htit class with a specified source. - /// - /// The data source object that publishes updates. public Htit(object source) : this() { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -77,6 +79,26 @@ public class Htit : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double CalculateSmoothedPrice(double p0, double p1, double p2, double p3) + { + return (4.0 * p0 + 3.0 * p1 + 2.0 * p2 + p3) * 0.1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double CalculateHilbertTransform(double b0, double b2, double b4, double b6, double adj) + { + return (0.0962 * (b0 - b6) + 0.5769 * (b2 - b4)) * adj; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double ClampPeriod(double pd, double lastPd) + { + pd = pd > PERIOD_UPPER_LIMIT * lastPd ? PERIOD_UPPER_LIMIT * lastPd : pd; + pd = pd < PERIOD_LOWER_LIMIT * lastPd ? PERIOD_LOWER_LIMIT * lastPd : pd; + return System.Math.Clamp(pd, MIN_PERIOD, MAX_PERIOD); + } + protected override double Calculation() { ManageState(Input.IsNew); @@ -100,65 +122,52 @@ public class Htit : AbstractBase return pr; } - double adj = (0.075 * _lastPd) + 0.54; + double adj = 0.075 * _lastPd + 0.54; // Smooth and detrender - double sp = ((4 * _priceBuffer[0]) + (3 * _priceBuffer[1]) + (2 * _priceBuffer[2]) + _priceBuffer[3]) / 10; + double sp = CalculateSmoothedPrice(_priceBuffer[0], _priceBuffer[1], _priceBuffer[2], _priceBuffer[3]); _spBuffer.Add(sp, Input.IsNew); - double dt = ((0.0962 * _spBuffer[0]) + (0.5769 * _spBuffer[2]) - (0.5769 * _spBuffer[4]) - (0.0962 * _spBuffer[6])) * adj; + double dt = CalculateHilbertTransform(_spBuffer[0], _spBuffer[2], _spBuffer[4], _spBuffer[6], adj); _dtBuffer.Add(dt, Input.IsNew); // In-phase and quadrature - double q1 = ((0.0962 * _dtBuffer[0]) + (0.5769 * _dtBuffer[2]) - (0.5769 * _dtBuffer[4]) - (0.0962 * _dtBuffer[6])) * adj; + double q1 = CalculateHilbertTransform(_dtBuffer[0], _dtBuffer[2], _dtBuffer[4], _dtBuffer[6], adj); _q1Buffer.Add(q1, Input.IsNew); double i1 = _dtBuffer[3]; _i1Buffer.Add(i1, Input.IsNew); // Advance the phases by 90 degrees - double jI = ((0.0962 * _i1Buffer[0]) + (0.5769 * _i1Buffer[2]) - (0.5769 * _i1Buffer[4]) - (0.0962 * _i1Buffer[6])) * adj; - double jQ = ((0.0962 * _q1Buffer[0]) + (0.5769 * _q1Buffer[2]) - (0.5769 * _q1Buffer[4]) - (0.0962 * _q1Buffer[6])) * adj; + double jI = CalculateHilbertTransform(_i1Buffer[0], _i1Buffer[2], _i1Buffer[4], _i1Buffer[6], adj); + double jQ = CalculateHilbertTransform(_q1Buffer[0], _q1Buffer[2], _q1Buffer[4], _q1Buffer[6], adj); // Phasor addition for 3-bar averaging - double i2 = i1 - jQ; - double q2 = q1 + jI; - - i2 = (0.2 * i2) + (0.8 * _i2Buffer[0]); - q2 = (0.2 * q2) + (0.8 * _q2Buffer[0]); + double i2 = ALPHA * (i1 - jQ) + BETA * _i2Buffer[0]; + double q2 = ALPHA * (q1 + jI) + BETA * _q2Buffer[0]; _i2Buffer.Add(i2, Input.IsNew); _q2Buffer.Add(q2, Input.IsNew); // Homodyne discriminator - double re = (i2 * _i2Buffer[1]) + (q2 * _q2Buffer[1]); - double im = (i2 * _q2Buffer[1]) - (q2 * _i2Buffer[1]); - - re = (0.2 * re) + (0.8 * _reBuffer[0]); - im = (0.2 * im) + (0.8 * _imBuffer[0]); + double re = ALPHA * (i2 * _i2Buffer[1] + q2 * _q2Buffer[1]) + BETA * _reBuffer[0]; + double im = ALPHA * (i2 * _q2Buffer[1] - q2 * _i2Buffer[1]) + BETA * _imBuffer[0]; _reBuffer.Add(re, Input.IsNew); _imBuffer.Add(im, Input.IsNew); // Calculate period - double pd = (im != 0 && re != 0) ? 2 * Math.PI / Math.Atan(im / re) : 0; - - // Adjust period to thresholds - pd = (pd > 1.5 * _lastPd) ? 1.5 * _lastPd : pd; - pd = (pd < 0.67 * _lastPd) ? 0.67 * _lastPd : pd; - pd = (pd < 6) ? 6 : pd; - pd = (pd > 50) ? 50 : pd; - - // Smooth the period - pd = (0.2 * pd) + (0.8 * _lastPd); + double pd = (im != 0 && re != 0) ? TWO_PI / System.Math.Atan(im / re) : 0; + pd = ClampPeriod(pd, _lastPd); + pd = ALPHA * pd + BETA * _lastPd; _pdBuffer.Add(pd, Input.IsNew); - double sd = (0.33 * pd) + (0.67 * _sdBuffer[0]); + double sd = 0.33 * pd + 0.67 * _sdBuffer[0]; _sdBuffer.Add(sd, Input.IsNew); // Smooth dominant cycle period int dcPeriods = (int)(sd + 0.5); - double sumPr = _priceBuffer.GetSpan().Slice(0, Math.Min(dcPeriods, _priceBuffer.Count)).ToArray().Sum(); + double sumPr = _priceBuffer.GetSpan().Slice(0, System.Math.Min(dcPeriods, _priceBuffer.Count)).ToArray().Sum(); double it = dcPeriods > 0 ? sumPr / dcPeriods : pr; _itBuffer.Add(it, Input.IsNew); @@ -166,9 +175,9 @@ public class Htit : AbstractBase _lastPd = pd; // Final indicator - if (_index >= 11) // 12th bar + if (_index >= 11) { - return ((4 * _itBuffer[0]) + (3 * _itBuffer[1]) + (2 * _itBuffer[2]) + _itBuffer[3]) / 10; + return CalculateSmoothedPrice(_itBuffer[0], _itBuffer[1], _itBuffer[2], _itBuffer[3]); } return pr; diff --git a/lib/averages/Hwma.cs b/lib/averages/Hwma.cs index 20c67659..8927b8eb 100644 --- a/lib/averages/Hwma.cs +++ b/lib/averages/Hwma.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -32,6 +32,8 @@ public class Hwma : AbstractBase { private readonly int _period; private readonly double _nA, _nB, _nC; + private readonly double _oneMinusNa, _oneMinusNb, _oneMinusNc; + private readonly double _halfA = 0.5; private double _pF, _pV, _pA; private double _ppF, _ppV, _ppA; @@ -56,12 +58,15 @@ public class Hwma : AbstractBase { if (period < 1) { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } _period = period; _nA = nA; _nB = nB; _nC = nC; + _oneMinusNa = 1.0 - nA; + _oneMinusNb = 1.0 - nB; + _oneMinusNc = 1.0 - nC; WarmupPeriod = period; Name = $"Hwma({_period})"; Init(); @@ -75,6 +80,7 @@ public class Hwma : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -82,6 +88,7 @@ public class Hwma : AbstractBase _ppF = _ppV = _ppA = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -100,6 +107,24 @@ public class Hwma : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateLevel(double input) + { + return _oneMinusNa * (_pF + _pV + _halfA * _pA) + _nA * input; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateVelocity(double F) + { + return _oneMinusNb * (_pV + _pA) + _nB * (F - _pF); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateAcceleration(double V) + { + return _oneMinusNc * _pA + _nC * (V - _pV); + } + protected override double Calculation() { ManageState(Input.IsNew); @@ -108,27 +133,25 @@ public class Hwma : AbstractBase { _pF = Input.Value; _pA = _pV = 0; + return Input.Value; } - double nA = _nA, nB = _nB, nC = _nC; if (_period == 1) { - nA = 1; - nB = 0; - nC = 0; + _pF = Input.Value; + _pV = _pA = 0; + return Input.Value; } - double F = (1 - nA) * (_pF + _pV + 0.5 * _pA) + nA * Input.Value; - double V = (1 - nB) * (_pV + _pA) + nB * (F - _pF); - double A = (1 - nC) * _pA + nC * (V - _pV); - - double hwma = F + V + 0.5 * A; + double F = CalculateLevel(Input.Value); + double V = CalculateVelocity(F); + double A = CalculateAcceleration(V); _pF = F; _pV = V; _pA = A; IsHot = _index >= WarmupPeriod; - return hwma; + return F + V + _halfA * A; } } diff --git a/lib/averages/Jma.cs b/lib/averages/Jma.cs index e250ca15..092e3607 100644 --- a/lib/averages/Jma.cs +++ b/lib/averages/Jma.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -32,12 +32,16 @@ public class Jma : AbstractBase private readonly double _phase; private readonly CircularBuffer _vsumBuff; private readonly CircularBuffer _avoltyBuff; - - private double _len1; - private double _pow1; private readonly double _beta; + private readonly double _len1; + private readonly double _pow1; + private readonly double _oneMinusAlpha; + private readonly double _oneMinusAlphaSquared; + private readonly double _alphaSquared; + private double _upperBand, _lowerBand, _p_upperBand, _p_lowerBand; - private double _prevMa1, _prevDet0, _prevDet1, _prevJma, _p_prevMa1, _p_prevDet0, _p_prevDet1, _p_prevJma; + private double _prevMa1, _prevDet0, _prevDet1, _prevJma; + private double _p_prevMa1, _p_prevDet0, _p_prevDet1, _p_prevJma; private double _vSum, _p_vSum; public double UpperBand { get; set; } @@ -45,57 +49,50 @@ public class Jma : AbstractBase public double Volty { get; set; } public double Factor { get; set; } - /// - /// Initializes a new instance of the Jma class with the specified parameters. - /// - /// The period over which to calculate the JMA. - /// The phase parameter (-100 to +100) controlling lag compensation. - /// The factor controlling volatility adaptation (default 0.45). - /// The size of the volatility buffer (default 10). - /// Thrown when period is less than 1. public Jma(int period, int phase = 0, double factor = 0.45, int buffer = 10) { if (period < 1) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); + throw new System.ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); } Factor = factor; _period = period; - _phase = Math.Clamp((phase * 0.01) + 1.5, 0.5, 2.5); + _phase = System.Math.Clamp((phase * 0.01) + 1.5, 0.5, 2.5); _vsumBuff = new CircularBuffer(buffer); _avoltyBuff = new CircularBuffer(65); - _beta = factor * (_period - 1) / (factor * (_period - 1) + 2); + _beta = factor * (period - 1) / (factor * (period - 1) + 2); + + _len1 = System.Math.Max((System.Math.Log(System.Math.Sqrt(period - 1)) / System.Math.Log(2.0)) + 2.0, 0); + _pow1 = System.Math.Max(_len1 - 2.0, 0.5); + + // Precalculate constants for alpha-based calculations + double alpha = System.Math.Pow(_beta, _pow1); + _oneMinusAlpha = 1.0 - alpha; + _oneMinusAlphaSquared = _oneMinusAlpha * _oneMinusAlpha; + _alphaSquared = alpha * alpha; WarmupPeriod = period * 2; Name = $"JMA({period})"; } - /// - /// Initializes a new instance of the Jma class with a specified source. - /// - /// The data source object that publishes updates. - /// The period over which to calculate the JMA. - /// The phase parameter (-100 to +100) controlling lag compensation. - /// The factor controlling volatility adaptation (default 0.45). - /// The size of the volatility buffer (default 10). public Jma(object source, int period, int phase = 0, double factor = 0.45, int buffer = 10) : this(period, phase, factor, buffer) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _upperBand = _lowerBand = 0.0; _p_upperBand = _p_lowerBand = 0.0; - _len1 = Math.Max((Math.Log(Math.Sqrt(_period - 1)) / Math.Log(2.0)) + 2.0, 0); - _pow1 = Math.Max(_len1 - 2.0, 0.5); _avoltyBuff.Clear(); _vsumBuff.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -121,6 +118,23 @@ public class Jma : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateVolatility(double price, double del1, double del2) + { + double volty = System.Math.Max(System.Math.Abs(del1), System.Math.Abs(del2)); + _vsumBuff.Add(volty, Input.IsNew); + _vSum += (_vsumBuff[^1] - _vsumBuff[0]) / _vsumBuff.Count; + _avoltyBuff.Add(_vSum, Input.IsNew); + return volty; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateRelativeVolatility(double volty, double avgVolty) + { + double rvolty = (avgVolty > 0) ? volty / avgVolty : 1; + return System.Math.Min(System.Math.Max(rvolty, 1.0), System.Math.Pow(_len1, 1.0 / _pow1)); + } + protected override double Calculation() { ManageState(Input.IsNew); @@ -130,35 +144,30 @@ public class Jma : AbstractBase { _upperBand = _lowerBand = price; _prevMa1 = _prevJma = price; + return price; } double del1 = price - _upperBand; double del2 = price - _lowerBand; - double volty = Math.Max(Math.Abs(del1), Math.Abs(del2)); + double volty = CalculateVolatility(price, del1, del2); + double avgVolty = _avoltyBuff.Average(); - _vsumBuff.Add(volty, Input.IsNew); - _vSum += (_vsumBuff[^1] - _vsumBuff[0]) / _vsumBuff.Count; - _avoltyBuff.Add(_vSum, Input.IsNew); - double avgvolty = _avoltyBuff.Average(); - - double rvolty = (avgvolty > 0) ? volty / avgvolty : 1; - rvolty = Math.Min(Math.Max(rvolty, 1.0), Math.Pow(_len1, 1.0 / _pow1)); - - double pow2 = Math.Pow(rvolty, _pow1); - double Kv = Math.Pow(_beta, Math.Sqrt(pow2)); + double rvolty = CalculateRelativeVolatility(volty, avgVolty); + double pow2 = System.Math.Pow(rvolty, _pow1); + double Kv = System.Math.Pow(_beta, System.Math.Sqrt(pow2)); _upperBand = (del1 >= 0) ? price : price - (Kv * del1); _lowerBand = (del2 <= 0) ? price : price - (Kv * del2); - double _alpha = Math.Pow(_beta, pow2); - double ma1 = Input.Value + _alpha * (_prevMa1 - Input.Value); + double alpha = System.Math.Pow(_beta, pow2); + double ma1 = price + alpha * (_prevMa1 - price); _prevMa1 = ma1; double det0 = price + _beta * (_prevDet0 - price + ma1) - ma1; _prevDet0 = det0; double ma2 = ma1 + _phase * det0; - double det1 = ((ma2 - _prevJma) * (1 - _alpha) * (1 - _alpha)) + (_alpha * _alpha * _prevDet1); + double det1 = ((ma2 - _prevJma) * _oneMinusAlphaSquared) + (_alphaSquared * _prevDet1); _prevDet1 = det1; double jma = _prevJma + det1; _prevJma = jma; diff --git a/lib/averages/Kama.cs b/lib/averages/Kama.cs index 7c15a827..1bd625c1 100644 --- a/lib/averages/Kama.cs +++ b/lib/averages/Kama.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -29,7 +29,8 @@ public class Kama : AbstractBase { private readonly int _period; private readonly double _scFast, _scSlow; - private CircularBuffer? _buffer; + private readonly double _scDiff; // Precalculated (_scFast - _scSlow) + private readonly CircularBuffer _buffer; private double _lastKama, _p_lastKama; /// The number of periods used to calculate the Efficiency Ratio. @@ -40,11 +41,13 @@ public class Kama : AbstractBase { if (period < 1) { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } _period = period; _scFast = 2.0 / (((period < fast) ? period : fast) + 1); _scSlow = 2.0 / (slow + 1); + _scDiff = _scFast - _scSlow; + _buffer = new CircularBuffer(_period + 1); WarmupPeriod = period; Name = $"Kama({_period}, {fast}, {slow})"; Init(); @@ -60,13 +63,15 @@ public class Kama : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); - _buffer = new CircularBuffer(_period + 1); + _buffer.Clear(); _lastKama = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -81,36 +86,50 @@ public class Kama : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateVolatility() + { + double volatility = 0; + for (int i = 1; i < _buffer.Count; i++) + { + volatility += System.Math.Abs(_buffer[i] - _buffer[i - 1]); + } + return volatility; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateEfficiencyRatio(double change, double volatility) + { + return volatility != 0 ? change / volatility : 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateSmoothingConstant(double er) + { + double sc = (er * _scDiff) + _scSlow; + return sc * sc; // Square the smoothing constant + } + protected override double Calculation() { ManageState(Input.IsNew); - _buffer!.Add(Input.Value, Input.IsNew); + _buffer.Add(Input.Value, Input.IsNew); - double kama; if (_index <= _period) { - kama = Input.Value; - } - else - { - double change = Math.Abs(_buffer[^1] - _buffer[0]); - double volatility = 0; - for (int i = 1; i < _buffer.Count; i++) - { - volatility += Math.Abs(_buffer[i] - _buffer[i - 1]); - } - - double er = volatility != 0 ? change / volatility : 0; - double sc = (er * (_scFast - _scSlow)) + _scSlow; - sc *= sc; // Square the smoothing constant - - kama = _lastKama + (sc * (Input.Value - _lastKama)); + _lastKama = Input.Value; + return Input.Value; } - _lastKama = kama; + double change = System.Math.Abs(_buffer[^1] - _buffer[0]); + double volatility = CalculateVolatility(); + double er = CalculateEfficiencyRatio(change, volatility); + double sc = CalculateSmoothingConstant(er); + + _lastKama += sc * (Input.Value - _lastKama); IsHot = _index >= WarmupPeriod; - return kama; + return _lastKama; } } diff --git a/lib/averages/Ltma.cs b/lib/averages/Ltma.cs index 910322ab..dd7c8415 100644 --- a/lib/averages/Ltma.cs +++ b/lib/averages/Ltma.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -29,6 +29,8 @@ namespace QuanTAlib; public class Ltma : AbstractBase { private readonly double _gamma; + private readonly double _oneMinusGamma; + private readonly double _invSix = 1.0 / 6.0; // Precalculated constant for final averaging private double _prevL0, _prevL1, _prevL2, _prevL3; private double _p_prevL0, _p_prevL1, _p_prevL2, _p_prevL3; @@ -42,8 +44,9 @@ public class Ltma : AbstractBase public Ltma(double gamma = 0.1) { if (gamma < 0 || gamma > 1) - throw new ArgumentOutOfRangeException(nameof(gamma), "Gamma must be between 0 and 1."); + throw new System.ArgumentOutOfRangeException(nameof(gamma), "Gamma must be between 0 and 1."); _gamma = gamma; + _oneMinusGamma = 1.0 - gamma; Name = $"Laguerre({gamma:F2})"; WarmupPeriod = 4; // Minimum number of samples needed Init(); @@ -57,12 +60,14 @@ public class Ltma : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _prevL0 = _prevL1 = _prevL2 = _prevL3 = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -82,24 +87,37 @@ public class Ltma : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateLaguerreStage(double input, double prev, double prevPrev) + { + return -_gamma * input + prev + _gamma * prevPrev; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CombineOutputs(double l0, double l1, double l2, double l3) + { + return (l0 + 2.0 * (l1 + l2) + l3) * _invSix; + } + protected override double Calculation() { ManageState(Input.IsNew); - // Laguerre filter calculation - double _l0 = (1 - _gamma) * Input.Value + _gamma * _prevL0; - double _l1 = -_gamma * _l0 + _prevL0 + _gamma * _prevL1; - double _l2 = -_gamma * _l1 + _prevL1 + _gamma * _prevL2; - double _l3 = -_gamma * _l2 + _prevL2 + _gamma * _prevL3; - _prevL0 = _l0; - _prevL1 = _l1; - _prevL2 = _l2; - _prevL3 = _l3; + // First stage + double l0 = _oneMinusGamma * Input.Value + _gamma * _prevL0; - double filteredValue = (_l0 + 2 * _l1 + 2 * _l2 + _l3) / 6; + // Subsequent stages using helper method + double l1 = CalculateLaguerreStage(l0, _prevL0, _prevL1); + double l2 = CalculateLaguerreStage(l1, _prevL1, _prevL2); + double l3 = CalculateLaguerreStage(l2, _prevL2, _prevL3); + + // Store values for next iteration + _prevL0 = l0; + _prevL1 = l1; + _prevL2 = l2; + _prevL3 = l3; IsHot = _index >= WarmupPeriod; - - return filteredValue; + return CombineOutputs(l0, l1, l2, l3); } } diff --git a/lib/averages/Maaf.cs b/lib/averages/Maaf.cs index b3ddc808..11c0fb81 100644 --- a/lib/averages/Maaf.cs +++ b/lib/averages/Maaf.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -33,11 +32,13 @@ public class Maaf : AbstractBase { private readonly CircularBuffer _priceBuffer; private readonly CircularBuffer _smoothBuffer; - private double _prevFilter, _prevValue2; private readonly double _threshold; - private double _p_prevFilter, _p_prevValue2; - private readonly int _period; + private readonly double _invSix = 1.0 / 6.0; + private readonly double[] _sortBuffer; // Pre-allocated buffer for sorting + + private double _prevFilter, _prevValue2; + private double _p_prevFilter, _p_prevValue2; /// The initial period for the filter (default 39). /// The threshold for adaptive adjustment (default 0.002). @@ -47,6 +48,7 @@ public class Maaf : AbstractBase _threshold = threshold; _priceBuffer = new CircularBuffer(4); _smoothBuffer = new CircularBuffer(period); + _sortBuffer = new double[period]; // Pre-allocate sorting buffer Name = "MAAF"; WarmupPeriod = period; Init(); @@ -61,15 +63,17 @@ public class Maaf : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { + base.Init(); _priceBuffer.Clear(); _smoothBuffer.Clear(); _prevFilter = 0; _prevValue2 = 0; - base.Init(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -86,6 +90,30 @@ public class Maaf : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateSmooth() + { + return (_priceBuffer[^1] + 2.0 * (_priceBuffer[^2] + _priceBuffer[^3]) + _priceBuffer[^4]) * _invSix; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double GetMedian(int length) + { + // Copy values to pre-allocated buffer + var span = _smoothBuffer.GetSpan().Slice(_smoothBuffer.Count - length, length); + span.CopyTo(_sortBuffer.AsSpan(0, length)); + + // Sort the required portion + System.Array.Sort(_sortBuffer, 0, length); + return _sortBuffer[length / 2]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double CalculateAlpha(int length) + { + return 2.0 / (length + 1); + } + protected override double Calculation() { ManageState(IsNew); @@ -97,7 +125,7 @@ public class Maaf : AbstractBase return Input.Value; } - double smooth = (_priceBuffer[^1] + (2 * _priceBuffer[^2]) + (2 * _priceBuffer[^3]) + _priceBuffer[^4]) / 6; + double smooth = CalculateSmooth(); _smoothBuffer.Add(smooth, Input.IsNew); if (_smoothBuffer.Count < _period) @@ -111,28 +139,23 @@ public class Maaf : AbstractBase while (value3 > _threshold && length > 0) { - double alpha = 2.0 / (length + 1); - - var sortedValues = _smoothBuffer.TakeLast(length).OrderBy(x => x).ToList(); - double value1 = sortedValues[length / 2]; + double alpha = CalculateAlpha(length); + double value1 = GetMedian(length); value2 = alpha * (smooth - _prevValue2) + _prevValue2; if (value1 != 0) { - value3 = Math.Abs(value1 - value2) / value1; + value3 = System.Math.Abs(value1 - value2) / value1; } length -= 2; } - if (length < 3) length = 3; - - double finalAlpha = 2.0 / (length + 1); + length = System.Math.Max(length, 3); + double finalAlpha = CalculateAlpha(length); double filter = finalAlpha * (smooth - _prevFilter) + _prevFilter; - _p_prevFilter = _prevFilter; _prevFilter = filter; - _p_prevValue2 = _prevValue2; _prevValue2 = value2; IsHot = _index >= WarmupPeriod; diff --git a/lib/averages/Mama.cs b/lib/averages/Mama.cs index 74e82a73..3722ab90 100644 --- a/lib/averages/Mama.cs +++ b/lib/averages/Mama.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -31,6 +31,12 @@ public class Mama : AbstractBase { private readonly double _fastLimit, _slowLimit; private readonly CircularBuffer _pr, _sm, _dt, _i1, _q1, _i2, _q2, _re, _im, _pd, _ph; + private readonly double _twoPi = 2.0 * System.Math.PI; + private readonly double _radToDeg = 180.0 / System.Math.PI; + private readonly double _alpha02 = 0.2; + private readonly double _alpha08 = 0.8; + private readonly double _famaAlpha = 0.5; + private double _mama, _fama; private double _prevMama, _prevFama, _sumPr; private double _p_prevMama, _p_prevFama, _p_sumPr; @@ -40,12 +46,9 @@ public class Mama : AbstractBase /// public TValue Fama { get; private set; } - /// The maximum adaptation speed (default 0.5). - /// The minimum adaptation speed (default 0.05). public Mama(double fastLimit = 0.5, double slowLimit = 0.05) { Fama = new TValue(); - Name = $"Mama({_fastLimit:F2}, {_slowLimit:F2})"; _fastLimit = fastLimit; _slowLimit = slowLimit; _pr = new(7); @@ -59,23 +62,23 @@ public class Mama : AbstractBase _im = new(2); _pd = new(2); _ph = new(2); + Name = $"Mama({_fastLimit:F2}, {_slowLimit:F2})"; Init(); } - /// The data source object that publishes updates. - /// The maximum adaptation speed (default 0.5). - /// The minimum adaptation speed (default 0.05). public Mama(object source, double fastLimit = 0.5, double slowLimit = 0.05) : this(fastLimit, slowLimit) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { Fama = new TValue(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -94,6 +97,33 @@ public class Mama : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateSmooth() + { + return (4.0 * _pr[^1] + 3.0 * _pr[^2] + 2.0 * _pr[^3] + _pr[^4]) * 0.1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double CalculateHilbertTransform(CircularBuffer buffer, double adj) + { + return (0.0962 * (buffer[^1] - buffer[^7]) + 0.5769 * (buffer[^3] - buffer[^5])) * adj; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculatePeriod(double im, double re) + { + if (im == 0 || re == 0) return _pd[^2]; + return _twoPi / System.Math.Atan(im / re); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double AdjustPeriod(double period) + { + period = System.Math.Clamp(period, 0.67 * _pd[^2], 1.5 * _pd[^2]); + period = System.Math.Clamp(period, 6.0, 50.0); + return _alpha02 * period + _alpha08 * _pd[^2]; + } + protected override double Calculation() { ManageState(Input.IsNew); @@ -102,85 +132,59 @@ public class Mama : AbstractBase if (_index > 6) { - double adj = (0.075 * _pd[^1]) + 0.54; + double adj = 0.075 * _pd[^1] + 0.54; - // Smooth - _sm.Add(((4 * _pr[^1]) + (3 * _pr[^2]) + (2 * _pr[^3]) + _pr[^4]) / 10, Input.IsNew); - - // Detrender - _dt.Add(((0.0962 * _sm[^1]) + (0.5769 * _sm[^3]) - (0.5769 * _sm[^5]) - (0.0962 * _sm[^7])) * adj, Input.IsNew); + // Smooth and Detrender + _sm.Add(CalculateSmooth(), Input.IsNew); + _dt.Add(CalculateHilbertTransform(_sm, adj), Input.IsNew); // In-phase and quadrature - _q1.Add(((0.0962 * _dt[^1]) + (0.5769 * _dt[^3]) - (0.5769 * _dt[^5]) - (0.0962 * _dt[^7])) * adj, Input.IsNew); + _q1.Add(CalculateHilbertTransform(_dt, adj), Input.IsNew); _i1.Add(_dt[^4], Input.IsNew); - // Advance the phases by 90 degrees - double jI = ((0.0962 * _i1[^1]) + (0.5769 * _i1[^3]) - (0.5769 * _i1[^5]) - (0.0962 * _i1[^7])) * adj; - double jQ = ((0.0962 * _q1[^1]) + (0.5769 * _q1[^3]) - (0.5769 * _q1[^5]) - (0.0962 * _q1[^7])) * adj; + // Advance phases + double jI = CalculateHilbertTransform(_i1, adj); + double jQ = CalculateHilbertTransform(_q1, adj); - // Phasor addition for 3-bar averaging - _i2.Add(_i1[^1] - jQ, Input.IsNew); - _q2.Add(_q1[^1] + jI, Input.IsNew); - _i2[^1] = 0.2 * _i2[^1] + 0.8 * _i2[^2]; - _q2[^1] = 0.2 * _q2[^1] + 0.8 * _q2[^2]; + // Phasor addition + double i2 = _i1[^1] - jQ; + double q2 = _q1[^1] + jI; + _i2.Add(i2, Input.IsNew); + _q2.Add(q2, Input.IsNew); + _i2[^1] = _alpha02 * _i2[^1] + _alpha08 * _i2[^2]; + _q2[^1] = _alpha02 * _q2[^1] + _alpha08 * _q2[^2]; // Homodyne discriminator - _re.Add((_i2[^1] * _i2[^2]) + (_q2[^1] * _q2[^2]), Input.IsNew); - _im.Add((_i2[^1] * _q2[^2]) - (_q2[^1] * _i2[^2]), Input.IsNew); - _re[^1] = (0.2 * _re[^1]) + (0.8 * _re[^2]); - _im[^1] = (0.2 * _im[^1]) + (0.8 * _im[^2]); + double re = _i2[^1] * _i2[^2] + _q2[^1] * _q2[^2]; + double im = _i2[^1] * _q2[^2] - _q2[^1] * _i2[^2]; + _re.Add(re, Input.IsNew); + _im.Add(im, Input.IsNew); + _re[^1] = _alpha02 * _re[^1] + _alpha08 * _re[^2]; + _im[^1] = _alpha02 * _im[^1] + _alpha08 * _im[^2]; - // Calculate period - if (_im[^1] != 0 && _re[^1] != 0) - { - _pd.Add(2 * Math.PI / Math.Atan(_im[^1] / _re[^1]), Input.IsNew); - } - else - { - _pd.Add(_pd[^2], Input.IsNew); - } + // Calculate and adjust period + double period = CalculatePeriod(_im[^1], _re[^1]); + _pd.Add(period, Input.IsNew); + _pd[^1] = AdjustPeriod(_pd[^1]); - // Adjust period to thresholds - _pd[^1] = Math.Max(Math.Min(_pd[^1], 1.5 * _pd[^2]), 0.67 * _pd[^2]); - _pd[^1] = Math.Max(Math.Min(_pd[^1], 50), 6); - _pd[^1] = (0.2 * _pd[^1]) + (0.8 * _pd[^2]); + // Phase calculation + double phase = _i1[^1] != 0 ? System.Math.Atan(_q1[^1] / _i1[^1]) * _radToDeg : _ph[^2]; + _ph.Add(phase, Input.IsNew); - // Determine phase position - if (_i1[^1] != 0) - { - _ph.Add(Math.Atan(_q1[^1] / _i1[^1]) * 180 / Math.PI, Input.IsNew); - } - else - { - _ph.Add(_ph[^2], Input.IsNew); - } - - // Change in phase - double delta = Math.Max(_ph[^2] - _ph[^1], 1); - - // Adaptive alpha value - double alpha = Math.Max(_fastLimit / delta, _slowLimit); + // Adaptive alpha + double delta = System.Math.Max(_ph[^2] - _ph[^1], 1.0); + double alpha = System.Math.Clamp(_fastLimit / delta, _slowLimit, _fastLimit); // Final indicators _mama = alpha * (_pr[^1] - _prevMama) + _prevMama; - _fama = 0.5 * alpha * (_mama - _prevFama) + _prevFama; + _fama = _famaAlpha * alpha * (_mama - _prevFama) + _prevFama; _prevMama = _mama; _prevFama = _fama; } else { - _pd.Add(0, Input.IsNew); - _sm.Add(0, Input.IsNew); - _dt.Add(0, Input.IsNew); - _i1.Add(0, Input.IsNew); - _q1.Add(0, Input.IsNew); - _i2.Add(0, Input.IsNew); - _q2.Add(0, Input.IsNew); - _re.Add(0, Input.IsNew); - _im.Add(0, Input.IsNew); - _ph.Add(0, Input.IsNew); - + InitializeBuffers(); _sumPr += Input.Value; _mama = _fama = _prevMama = _prevFama = _sumPr / _index; } @@ -190,4 +194,19 @@ public class Mama : AbstractBase return _mama; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void InitializeBuffers() + { + _pd.Add(0, Input.IsNew); + _sm.Add(0, Input.IsNew); + _dt.Add(0, Input.IsNew); + _i1.Add(0, Input.IsNew); + _q1.Add(0, Input.IsNew); + _i2.Add(0, Input.IsNew); + _q2.Add(0, Input.IsNew); + _re.Add(0, Input.IsNew); + _im.Add(0, Input.IsNew); + _ph.Add(0, Input.IsNew); + } } diff --git a/lib/averages/Mgdi.cs b/lib/averages/Mgdi.cs index 70ec070c..efa64a61 100644 --- a/lib/averages/Mgdi.cs +++ b/lib/averages/Mgdi.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -29,6 +29,7 @@ public class Mgdi : AbstractBase { private readonly int _period; private readonly double _kFactor; + private readonly double _kFactorPeriod; // Precalculated k * period private double _prevMd, _p_prevMd; /// The number of periods used in the MGDI calculation. @@ -38,14 +39,15 @@ public class Mgdi : AbstractBase { if (period <= 0) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0."); + throw new System.ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0."); } if (kFactor <= 0) { - throw new ArgumentOutOfRangeException(nameof(kFactor), "K-Factor must be greater than 0."); + throw new System.ArgumentOutOfRangeException(nameof(kFactor), "K-Factor must be greater than 0."); } _period = period; _kFactor = kFactor; + _kFactorPeriod = kFactor * period; Name = "Mgdi"; WarmupPeriod = period; Init(); @@ -60,12 +62,14 @@ public class Mgdi : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _prevMd = _p_prevMd = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -79,6 +83,18 @@ public class Mgdi : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateRatio(double value) + { + return _prevMd != 0 ? value / _prevMd : 1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateMd(double value, double ratio) + { + return _prevMd + ((value - _prevMd) / (_kFactorPeriod * System.Math.Pow(ratio, 4))); + } + protected override double Calculation() { ManageState(Input.IsNew); @@ -90,10 +106,8 @@ public class Mgdi : AbstractBase } else { - double ratio = _prevMd != 0 ? value / _prevMd : 1; - double md = _prevMd + ((value - _prevMd) / - (_kFactor * _period * Math.Pow(ratio, 4))); - _prevMd = md; + double ratio = CalculateRatio(value); + _prevMd = CalculateMd(value, ratio); } IsHot = _index >= _period; diff --git a/lib/averages/Mma.cs b/lib/averages/Mma.cs index 99d92ab3..c999ee09 100644 --- a/lib/averages/Mma.cs +++ b/lib/averages/Mma.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -29,6 +29,9 @@ public class Mma : AbstractBase { private readonly int _period; private readonly CircularBuffer _buffer; + private readonly double _periodRecip; // 1/period + private readonly double _combinedRecip; // 6/((period+1)*period) + private readonly double[] _weights; // Precalculated weights private double _lastMma; /// The number of periods used in the MMA calculation. Must be at least 2. @@ -37,10 +40,20 @@ public class Mma : AbstractBase { if (period < 2) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2."); + throw new System.ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2."); } _period = period; _buffer = new CircularBuffer(period); + _periodRecip = 1.0 / period; + _combinedRecip = 6.0 / ((period + 1) * period); + + // Precalculate weights + _weights = new double[period]; + for (int i = 0; i < period; i++) + { + _weights[i] = (period - (2 * i + 1)) * 0.5; + } + Name = "Mma"; WarmupPeriod = period; Init(); @@ -54,6 +67,7 @@ public class Mma : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -61,6 +75,7 @@ public class Mma : AbstractBase _buffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -69,6 +84,17 @@ public class Mma : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateWeightedSum() + { + double sum = 0; + for (int i = 0; i < _period; i++) + { + sum += _weights[i] * _buffer[^(i + 1)]; + } + return sum; + } + protected override double Calculation() { ManageState(Input.IsNew); @@ -78,7 +104,7 @@ public class Mma : AbstractBase { double T = _buffer.Sum(); double S = CalculateWeightedSum(); - _lastMma = (T / _period) + (6 * S) / ((_period + 1) * _period); + _lastMma = (T * _periodRecip) + (S * _combinedRecip); } else { @@ -89,20 +115,4 @@ public class Mma : AbstractBase IsHot = _index >= _period; return _lastMma; } - - /// - /// Calculates the weighted sum component of the MMA. - /// The weights are symmetric around the center, decreasing linearly from the center outward. - /// - /// The weighted sum of the data points. - private double CalculateWeightedSum() - { - double sum = 0; - for (int i = 0; i < _period; i++) - { - double weight = (_period - (2 * i + 1)) / 2.0; - sum += weight * _buffer[^(i + 1)]; - } - return sum; - } } diff --git a/lib/averages/Pwma.cs b/lib/averages/Pwma.cs index 73c27470..c06b82f0 100644 --- a/lib/averages/Pwma.cs +++ b/lib/averages/Pwma.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -31,6 +30,7 @@ public class Pwma : AbstractBase { private readonly int _period; private readonly Convolution _convolution; + private readonly double[] _kernel; /// The number of data points used in the PWMA calculation. /// Thrown when period is less than 1. @@ -38,10 +38,11 @@ public class Pwma : AbstractBase { if (period < 1) { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } _period = period; - _convolution = new Convolution(GenerateKernel(_period)); + _kernel = GenerateKernel(_period); + _convolution = new Convolution(_kernel); Name = "Pwma"; WarmupPeriod = period; Init(); @@ -55,12 +56,14 @@ public class Pwma : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private new void Init() { base.Init(); _convolution.Init(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -70,24 +73,33 @@ public class Pwma : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double CalculateKernelSum(double[] kernel, int length) + { + double sum = 0; + for (int i = 0; i < length; i++) + { + sum += kernel[i]; + } + return sum; + } + protected override double Calculation() { ManageState(Input.IsNew); // Use Convolution for calculation - TValue convolutionResult = _convolution.Calc(Input); - + var convolutionResult = _convolution.Calc(Input); double result = convolutionResult.Value; // Adjust for partial periods during warmup if (_index < _period) { double[] partialKernel = GenerateKernel(_index); - result /= partialKernel.Sum(); + result *= CalculateKernelSum(_kernel, _period) / CalculateKernelSum(partialKernel, _index); } IsHot = _index >= WarmupPeriod; - return result; } @@ -96,11 +108,13 @@ public class Pwma : AbstractBase /// /// The period for which to generate the kernel. /// An array of normalized Pascal's triangle-based weights for the convolution operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double[] GenerateKernel(int period) { double[] kernel = new double[period]; kernel[0] = 1; + // Generate Pascal's triangle coefficients for (int i = 1; i < period; i++) { for (int j = i; j > 0; j--) @@ -109,11 +123,13 @@ public class Pwma : AbstractBase } } - // Normalize the kernel - double weightSum = kernel.Sum(); + // Calculate sum and normalize in one pass + double weightSum = CalculateKernelSum(kernel, period); + double invWeightSum = 1.0 / weightSum; + for (int i = 0; i < period; i++) { - kernel[i] /= weightSum; + kernel[i] *= invWeightSum; } return kernel; diff --git a/lib/averages/Qema.cs b/lib/averages/Qema.cs index cdf08f82..66cc7107 100644 --- a/lib/averages/Qema.cs +++ b/lib/averages/Qema.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -41,7 +41,7 @@ public class Qema : AbstractBase { if (k1 <= 0 || k2 <= 0 || k3 <= 0 || k4 <= 0) { - throw new ArgumentOutOfRangeException(nameof(k1), "All k values must be in the range (0, 1]."); + throw new System.ArgumentOutOfRangeException(nameof(k1), "All k values must be in the range (0, 1]."); } _ema1 = new Ema(k1); @@ -50,8 +50,7 @@ public class Qema : AbstractBase _ema4 = new Ema(k4); Name = $"QEMA ({k1:F2},{k2:F2},{k3:F2},{k4:F2})"; - double smK = Math.Min(Math.Min(k1, k2), Math.Min(k3, k4)); - + double smK = System.Math.Min(System.Math.Min(k1, k2), System.Math.Min(k3, k4)); WarmupPeriod = (int)((2 - smK) / smK); Init(); } @@ -68,6 +67,7 @@ public class Qema : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -75,6 +75,7 @@ public class Qema : AbstractBase _p_lastQema = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -88,16 +89,25 @@ public class Qema : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateEma(Ema ema, double value) + { + var tempValue = new TValue(Input.Time, value, Input.IsNew); + return ema.Calc(tempValue).Value; + } + protected override double Calculation() { ManageState(Input.IsNew); - double ema1 = _ema1.Calc(new TValue(Input.Time, Input.Value, Input.IsNew)); - double ema2 = _ema2.Calc(new TValue(Input.Time, ema1, Input.IsNew)); - double ema3 = _ema3.Calc(new TValue(Input.Time, ema2, Input.IsNew)); - double ema4 = _ema4.Calc(new TValue(Input.Time, ema3, Input.IsNew)); + // Calculate EMAs in sequence + double ema1 = CalculateEma(_ema1, Input.Value); + double ema2 = CalculateEma(_ema2, ema1); + double ema3 = CalculateEma(_ema3, ema2); + double ema4 = CalculateEma(_ema4, ema3); - _lastQema = 4 * ema1 - 6 * ema2 + 4 * ema3 - ema4; + // Combine EMAs using optimized formula + _lastQema = 4.0 * (ema1 + ema3) - (6.0 * ema2 + ema4); IsHot = _index >= WarmupPeriod; return _lastQema; diff --git a/lib/averages/Rema.cs b/lib/averages/Rema.cs index b8420a3d..65c76f70 100644 --- a/lib/averages/Rema.cs +++ b/lib/averages/Rema.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -29,6 +29,7 @@ public class Rema : AbstractBase { private readonly int _period; private readonly double _lambda; + private readonly double _lambdaPlus1Recip; // 1/(1 + lambda) private double _lastRema, _prevRema; private double _savedLastRema, _savedPrevRema; @@ -48,12 +49,13 @@ public class Rema : AbstractBase public Rema(int period, double lambda = 0.5) { if (period < 1) - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); + throw new System.ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); if (lambda < 0) - throw new ArgumentOutOfRangeException(nameof(lambda), "Lambda must be non-negative."); + throw new System.ArgumentOutOfRangeException(nameof(lambda), "Lambda must be non-negative."); _period = period; _lambda = lambda; + _lambdaPlus1Recip = 1.0 / (1.0 + lambda); Name = $"REMA({period},{lambda:F2})"; WarmupPeriod = period; Init(); @@ -68,6 +70,7 @@ public class Rema : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -77,6 +80,7 @@ public class Rema : AbstractBase _savedPrevRema = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -92,15 +96,28 @@ public class Rema : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateAlpha() + { + return 2.0 / (System.Math.Min(_period, _index) + 1); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateRema(double alpha, double input) + { + double standardTerm = _lastRema + alpha * (input - _lastRema); + double regularizationTerm = _lastRema + (_lastRema - _prevRema); + return (standardTerm + _lambda * regularizationTerm) * _lambdaPlus1Recip; + } + protected override double Calculation() { ManageState(Input.IsNew); - double alpha = 2.0 / (Math.Min(_period, _index) + 1); - if (_index > 2) { - double rema = (_lastRema + alpha * (Input.Value - _lastRema) + _lambda * (_lastRema + (_lastRema - _prevRema))) / (1 + _lambda); + double alpha = CalculateAlpha(); + double rema = CalculateRema(alpha, Input.Value); _prevRema = _lastRema; _lastRema = rema; } @@ -110,7 +127,7 @@ public class Rema : AbstractBase _lastRema = Input.Value; } else - { // _index == 1 + { _lastRema = Input.Value; } diff --git a/lib/averages/Rma.cs b/lib/averages/Rma.cs index 628d8310..71dc6a76 100644 --- a/lib/averages/Rma.cs +++ b/lib/averages/Rma.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -20,44 +21,17 @@ namespace QuanTAlib; /// public class Rma : AbstractBase { - // inherited _index - // inherited _value - - /// - /// The period for the RMA calculation. - /// private readonly int _period; - - /// - /// Circular buffer for SMA calculation. - /// + private readonly double _k; // Wilder's smoothing factor + private readonly double _oneMinusK; // 1 - k + private readonly double _epsilon = 1e-10; + private readonly bool _useSma; private CircularBuffer _sma; - /// - /// The last calculated RMA value. - /// private double _lastRma, _p_lastRma; - - /// - /// Compensator for early RMA values. - /// private double _e, _p_e; - - /// - /// The smoothing factor for RMA calculation. - /// - private readonly double _k; - - /// - /// Flags to track initialization status. - /// private bool _isInit, _p_isInit; - /// - /// Flag to determine whether to use SMA for initial values. - /// - private readonly bool _useSma; - /// /// Initializes a new instance of the Rma class with a specified period. /// @@ -68,14 +42,15 @@ public class Rma : AbstractBase { if (period < 1) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); + throw new System.ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); } _period = period; - _k = 1.0 / _period; // Wilder's smoothing factor + _k = 1.0 / period; + _oneMinusK = 1.0 - _k; _useSma = useSma; _sma = new(period); Name = "Rma"; - WarmupPeriod = _period * 2; // RMA typically needs more warmup periods + WarmupPeriod = period * 2; // RMA typically needs more warmup periods Init(); } @@ -91,9 +66,7 @@ public class Rma : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - /// - /// Initializes the Rma instance. - /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -104,10 +77,7 @@ public class Rma : AbstractBase _sma = new(_period); } - /// - /// Manages the state of the Rma instance. - /// - /// Indicates whether the input is new. + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -125,21 +95,30 @@ public class Rma : AbstractBase } } - /// - /// Performs the RMA calculation. - /// - /// The calculated RMA value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateRma(double input) + { + return _k * input + _oneMinusK * _lastRma; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CompensateRma(double rma) + { + _e = (_e > _epsilon) ? _oneMinusK * _e : 0; + return (_useSma || _e <= double.Epsilon) ? rma : rma / (1.0 - _e); + } + protected override double Calculation() { - double result, _rma; ManageState(Input.IsNew); - // when _UseSma == true, use SMA calculation until we have enough data points + double result; if (!_isInit && _useSma) { _sma.Add(Input.Value, Input.IsNew); - _rma = _sma.Average(); - result = _rma; + _lastRma = _sma.Average(); + result = _lastRma; + if (_index >= _period) { _isInit = true; @@ -147,15 +126,10 @@ public class Rma : AbstractBase } else { - // compensator for early rma values - _e = (_e > 1e-10) ? (1 - _k) * _e : 0; - - _rma = _k * Input.Value + (1 - _k) * _lastRma; - - // _useSma decides if we use compensator or not - result = (_useSma || _e <= double.Epsilon) ? _rma : _rma / (1 - _e); + _lastRma = CalculateRma(Input.Value); + result = CompensateRma(_lastRma); } - _lastRma = _rma; + IsHot = _index >= WarmupPeriod; return result; } diff --git a/lib/averages/Sinema.cs b/lib/averages/Sinema.cs index 64efde2e..a58d59b3 100644 --- a/lib/averages/Sinema.cs +++ b/lib/averages/Sinema.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -29,6 +29,7 @@ namespace QuanTAlib; public class Sinema : AbstractBase { private readonly Convolution _convolution; + private readonly double[] _kernel; /// The number of data points used in the SINEMA calculation. /// Thrown when period is less than 1. @@ -36,9 +37,10 @@ public class Sinema : AbstractBase { if (period < 1) { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } - _convolution = new Convolution(GenerateKernel(period)); + _kernel = GenerateKernel(period); + _convolution = new Convolution(_kernel); Name = "Sinema"; WarmupPeriod = period; Init(); @@ -52,12 +54,14 @@ public class Sinema : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private new void Init() { base.Init(); _convolution.Init(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -67,42 +71,43 @@ public class Sinema : AbstractBase } } - protected override double Calculation() - { - ManageState(Input.IsNew); - - // Use Convolution for calculation - TValue convolutionResult = _convolution.Calc(Input); - - double result = convolutionResult.Value; - IsHot = _index >= WarmupPeriod; - - return result; - } - /// /// Generates the sine-based convolution kernel for the SINEMA calculation. /// /// The period for which to generate the kernel. /// An array of normalized sine-based weights for the convolution operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double[] GenerateKernel(int period) { double[] kernel = new double[period]; double weightSum = 0; + double piDivPeriodPlus1 = System.Math.PI / (period + 1); + // Calculate weights and sum in one pass for (int i = 0; i < period; i++) { - // Use sine function to generate weights - kernel[i] = Math.Sin((i + 1) * Math.PI / (period + 1)); + kernel[i] = System.Math.Sin((i + 1) * piDivPeriodPlus1); weightSum += kernel[i]; } - // Normalize the kernel + // Normalize using multiplication instead of division + double invWeightSum = 1.0 / weightSum; for (int i = 0; i < period; i++) { - kernel[i] /= weightSum; + kernel[i] *= invWeightSum; } return kernel; } + + protected override double Calculation() + { + ManageState(Input.IsNew); + + // Use Convolution for calculation + var convolutionResult = _convolution.Calc(Input); + IsHot = _index >= WarmupPeriod; + + return convolutionResult.Value; + } } diff --git a/lib/averages/Sma.cs b/lib/averages/Sma.cs index 6c797162..784abfd7 100644 --- a/lib/averages/Sma.cs +++ b/lib/averages/Sma.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -27,9 +27,8 @@ namespace QuanTAlib; public class Sma : AbstractBase { - // inherited _index - // inherited _value private readonly CircularBuffer _buffer; + private readonly int _period; /// The number of data points used in the SMA calculation. /// Thrown when period is less than 1. @@ -37,9 +36,9 @@ public class Sma : AbstractBase { if (period < 1) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); + throw new System.ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); } - WarmupPeriod = period; + _period = period; _buffer = new CircularBuffer(period); Name = "Sma"; WarmupPeriod = period; @@ -48,12 +47,13 @@ public class Sma : AbstractBase /// The data source object that publishes updates. /// The number of data points used in the SMA calculation. - public Sma(object source, int period) : this(period: period) + public Sma(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -69,12 +69,10 @@ public class Sma : AbstractBase /// The calculated SMA value. protected override double Calculation() { - double result; ManageState(IsNew); _buffer.Add(Input.Value, Input.IsNew); - result = _buffer.Average(); IsHot = _index >= WarmupPeriod; - return result; + return _buffer.Average(); } } diff --git a/lib/averages/Smma.cs b/lib/averages/Smma.cs index f66262b0..a508acff 100644 --- a/lib/averages/Smma.cs +++ b/lib/averages/Smma.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -28,7 +28,9 @@ namespace QuanTAlib; public class Smma : AbstractBase { private readonly int _period; - private CircularBuffer? _buffer; + private readonly double _periodRecip; // 1/period + private readonly double _periodMinusOne; // period-1 + private readonly CircularBuffer _buffer; private double _lastSmma, _p_lastSmma; /// The number of data points used in the SMMA calculation. @@ -37,9 +39,12 @@ public class Smma : AbstractBase { if (period < 1) { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } _period = period; + _periodRecip = 1.0 / period; + _periodMinusOne = period - 1; + _buffer = new CircularBuffer(period); WarmupPeriod = period; Name = $"Smma({_period})"; Init(); @@ -53,13 +58,15 @@ public class Smma : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); - _buffer = new CircularBuffer(_period); + _buffer.Clear(); _lastSmma = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -74,18 +81,21 @@ public class Smma : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateSmma(double input) + { + return (_lastSmma * _periodMinusOne + input) * _periodRecip; + } + protected override double Calculation() { ManageState(Input.IsNew); - - _buffer!.Add(Input.Value, Input.IsNew); + _buffer.Add(Input.Value, Input.IsNew); double smma; - if (_index <= _period) { smma = _buffer.Average(); - if (_index == _period) { _lastSmma = smma; // Initialize _lastSmma for the transition @@ -93,7 +103,7 @@ public class Smma : AbstractBase } else { - smma = ((_lastSmma * (_period - 1)) + Input.Value) / _period; + smma = CalculateSmma(Input.Value); } _lastSmma = smma; diff --git a/lib/averages/T3.cs b/lib/averages/T3.cs index 49a00cb5..f0d3402f 100644 --- a/lib/averages/T3.cs +++ b/lib/averages/T3.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -31,8 +31,10 @@ public class T3 : AbstractBase private readonly int _period; private readonly double _vfactor; private readonly bool _useSma; - private readonly double _k, _k1m, _c1, _c2, _c3, _c4; + private readonly double _k; + private readonly double _c1, _c2, _c3, _c4; private readonly CircularBuffer _buffer1, _buffer2, _buffer3, _buffer4, _buffer5, _buffer6; + private double _lastEma1, _lastEma2, _lastEma3, _lastEma4, _lastEma5, _lastEma6; private double _p_lastEma1, _p_lastEma2, _p_lastEma3, _p_lastEma4, _p_lastEma5, _p_lastEma6; @@ -44,7 +46,7 @@ public class T3 : AbstractBase { if (period < 1) { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } _period = period; _vfactor = vfactor; @@ -52,11 +54,14 @@ public class T3 : AbstractBase WarmupPeriod = period; _k = 2.0 / (_period + 1); - _k1m = 1.0 - _k; - _c1 = -_vfactor * _vfactor * _vfactor; - _c2 = 3 * _vfactor * _vfactor + 3 * _vfactor * _vfactor * _vfactor; - _c3 = -6 * _vfactor * _vfactor - 3 * _vfactor - 3 * _vfactor * _vfactor * _vfactor; - _c4 = 1 + 3 * _vfactor + _vfactor * _vfactor * _vfactor + 3 * _vfactor * _vfactor; + + // Precalculate coefficients + double v2 = vfactor * vfactor; + double v3 = v2 * vfactor; + _c1 = -v3; + _c2 = 3.0 * (v2 + v3); + _c3 = -3.0 * (2.0 * v2 + vfactor + v3); + _c4 = 1.0 + 3.0 * vfactor + v3 + 3.0 * v2; _buffer1 = new(period); _buffer2 = new(period); @@ -79,6 +84,7 @@ public class T3 : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { _lastEma1 = _lastEma2 = _lastEma3 = _lastEma4 = _lastEma5 = _lastEma6 = 0; @@ -90,6 +96,7 @@ public class T3 : AbstractBase _buffer6.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -114,6 +121,18 @@ public class T3 : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateEma(double input, double lastEma) + { + return _k * (input - lastEma) + lastEma; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateT3(double ema3, double ema4, double ema5, double ema6) + { + return _c1 * ema6 + _c2 * ema5 + _c3 * ema4 + _c4 * ema3; + } + protected override double Calculation() { ManageState(Input.IsNew); @@ -141,12 +160,12 @@ public class T3 : AbstractBase } else { - ema1 = _k * (Input.Value - _lastEma1) + _lastEma1; - ema2 = _k * (ema1 - _lastEma2) + _lastEma2; - ema3 = _k * (ema2 - _lastEma3) + _lastEma3; - ema4 = _k * (ema3 - _lastEma4) + _lastEma4; - ema5 = _k * (ema4 - _lastEma5) + _lastEma5; - ema6 = _k * (ema5 - _lastEma6) + _lastEma6; + ema1 = CalculateEma(Input.Value, _lastEma1); + ema2 = CalculateEma(ema1, _lastEma2); + ema3 = CalculateEma(ema2, _lastEma3); + ema4 = CalculateEma(ema3, _lastEma4); + ema5 = CalculateEma(ema4, _lastEma5); + ema6 = CalculateEma(ema5, _lastEma6); } _lastEma1 = ema1; @@ -156,9 +175,7 @@ public class T3 : AbstractBase _lastEma5 = ema5; _lastEma6 = ema6; - double t3 = _c1 * ema6 + _c2 * ema5 + _c3 * ema4 + _c4 * ema3; - IsHot = _index >= WarmupPeriod; - return t3; + return CalculateT3(ema3, ema4, ema5, ema6); } } diff --git a/lib/averages/Tema.cs b/lib/averages/Tema.cs index 1090cd2a..ac85723c 100644 --- a/lib/averages/Tema.cs +++ b/lib/averages/Tema.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -29,10 +29,13 @@ namespace QuanTAlib; public class Tema : AbstractBase { private readonly int _period; + private readonly double _k; + private readonly double _oneMinusK; + private readonly double _epsilon = 1e-10; private double _lastEma1, _p_lastEma1; private double _lastEma2, _p_lastEma2; private double _lastEma3, _p_lastEma3; - private double _k, _e, _p_e; + private double _e, _p_e; /// The number of periods used in each EMA calculation. /// Thrown when period is less than 1. @@ -40,12 +43,14 @@ public class Tema : AbstractBase { if (period < 1) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); + throw new System.ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); } _period = period; + _k = 2.0 / (_period + 1); + _oneMinusK = 1.0 - _k; Name = "Tema"; double percentile = 0.85; //targeting 85th percentile of correctness of converging EMA - WarmupPeriod = (int)Math.Ceiling(-period * Math.Log(1 - percentile)); + WarmupPeriod = (int)System.Math.Ceiling(-period * System.Math.Log(1 - percentile)); Init(); } @@ -57,14 +62,15 @@ public class Tema : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); - _k = 2.0 / (_period + 1); _e = 1.0; _lastEma1 = _lastEma2 = _lastEma3 = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -84,24 +90,37 @@ public class Tema : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateEma(double input, double lastEma, double invE) + { + return _k * (input * invE - lastEma) + lastEma; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double UpdateCompensator() + { + _e = (_e > _epsilon) ? _oneMinusK * _e : 0; + return (_e > _epsilon) ? 1.0 / (1.0 - _e) : 1.0; + } + protected override double Calculation() { - double result, _ema1, _ema2, _ema3; ManageState(Input.IsNew); - _e = (_e > 1e-10) ? (1 - _k) * _e : 0; - double _invE = (_e > 1e-10) ? 1 / (1 - _e) : 1; + double invE = UpdateCompensator(); - _ema1 = _k * (Input.Value - _lastEma1) + _lastEma1; - _ema2 = _k * (_ema1 * _invE - _lastEma2) + _lastEma2; - _ema3 = _k * (_ema2 * _invE - _lastEma3) + _lastEma3; + // Calculate EMAs with compensation + double ema1 = CalculateEma(Input.Value, _lastEma1, 1.0); // First EMA doesn't need compensation + double ema2 = CalculateEma(ema1, _lastEma2, invE); + double ema3 = CalculateEma(ema2, _lastEma3, invE); - double _tema = 3 * _ema1 * _invE - 3 * _ema2 * _invE + _ema3 * _invE; + // Store values for next iteration + _lastEma1 = ema1; + _lastEma2 = ema2; + _lastEma3 = ema3; - result = _tema; - _lastEma1 = _ema1; - _lastEma2 = _ema2; - _lastEma3 = _ema3; + // Calculate final TEMA with compensation + double result = (3.0 * ema1 - 3.0 * ema2 + ema3) * invE; IsHot = _index >= WarmupPeriod; return result; diff --git a/lib/averages/Trima.cs b/lib/averages/Trima.cs index 93e8a17b..cc4ccb3c 100644 --- a/lib/averages/Trima.cs +++ b/lib/averages/Trima.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -29,6 +29,7 @@ namespace QuanTAlib; public class Trima : AbstractBase { private readonly Convolution _convolution; + private readonly double[] _kernel; /// The number of data points used in the TRIMA calculation. /// Thrown when period is less than 1. @@ -36,9 +37,10 @@ public class Trima : AbstractBase { if (period < 1) { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } - _convolution = new Convolution(GenerateKernel(period)); + _kernel = GenerateKernel(period); + _convolution = new Convolution(_kernel); Name = "Trima"; WarmupPeriod = period; Init(); @@ -57,33 +59,38 @@ public class Trima : AbstractBase /// /// The period for which to generate the kernel. /// An array of normalized triangular weights for the convolution operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double[] GenerateKernel(int period) { double[] kernel = new double[period]; int halfPeriod = (period + 1) / 2; double weightSum = 0; + // Calculate weights and sum in one pass for (int i = 0; i < period; i++) { kernel[i] = i < halfPeriod ? i + 1 : period - i; weightSum += kernel[i]; } - // Normalize the kernel + // Normalize using multiplication instead of division + double invWeightSum = 1.0 / weightSum; for (int i = 0; i < period; i++) { - kernel[i] /= weightSum; + kernel[i] *= invWeightSum; } return kernel; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private new void Init() { base.Init(); _convolution.Init(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -98,11 +105,9 @@ public class Trima : AbstractBase ManageState(Input.IsNew); // Use Convolution for calculation - TValue convolutionResult = _convolution.Calc(Input); - - double result = convolutionResult.Value; + var convolutionResult = _convolution.Calc(Input); IsHot = _index >= WarmupPeriod; - return result; + return convolutionResult.Value; } } diff --git a/lib/averages/Vidya.cs b/lib/averages/Vidya.cs index 125b3e68..1d8b64d3 100644 --- a/lib/averages/Vidya.cs +++ b/lib/averages/Vidya.cs @@ -1,7 +1,4 @@ -using System; -using System.Linq; using System.Runtime.CompilerServices; - namespace QuanTAlib; /// @@ -33,9 +30,9 @@ public class Vidya : AbstractBase { private readonly int _longPeriod; private readonly double _alpha; + private readonly CircularBuffer _shortBuffer; + private readonly CircularBuffer _longBuffer; private double _lastVIDYA, _p_lastVIDYA; - private readonly CircularBuffer? _shortBuffer; - private readonly CircularBuffer? _longBuffer; /// The number of periods for short-term volatility calculation. /// The number of periods for long-term volatility calculation (default is 4x shortPeriod). @@ -45,14 +42,14 @@ public class Vidya : AbstractBase { if (shortPeriod < 1) { - throw new ArgumentException("Short period must be greater than or equal to 1.", nameof(shortPeriod)); + throw new System.ArgumentException("Short period must be greater than or equal to 1.", nameof(shortPeriod)); } _longPeriod = (longPeriod == 0) ? shortPeriod * 4 : longPeriod; _alpha = alpha; - WarmupPeriod = _longPeriod; - Name = $"Vidya({shortPeriod},{_longPeriod})"; _shortBuffer = new CircularBuffer(shortPeriod); _longBuffer = new CircularBuffer(_longPeriod); + WarmupPeriod = _longPeriod; + Name = $"Vidya({shortPeriod},{_longPeriod})"; Init(); } @@ -67,12 +64,14 @@ public class Vidya : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _lastVIDYA = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -87,12 +86,35 @@ public class Vidya : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double CalculateStdDev(CircularBuffer buffer) + { + double mean = buffer.Average(); + double sumSquaredDiff = 0; + var span = buffer.GetSpan(); + + for (int i = 0; i < buffer.Count; i++) + { + double diff = span[i] - mean; + sumSquaredDiff += diff * diff; + } + + return System.Math.Sqrt(sumSquaredDiff / buffer.Count); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateVidya(double shortStdDev, double longStdDev) + { + double s = _alpha * (shortStdDev / longStdDev); + return (s * Input.Value) + ((1.0 - s) * _lastVIDYA); + } + protected override double Calculation() { ManageState(Input.IsNew); - _shortBuffer!.Add(Input.Value, Input.IsNew); - _longBuffer!.Add(Input.Value, Input.IsNew); + _shortBuffer.Add(Input.Value, Input.IsNew); + _longBuffer.Add(Input.Value, Input.IsNew); double vidya; if (_index <= _longPeriod) @@ -103,8 +125,7 @@ public class Vidya : AbstractBase { double shortStdDev = CalculateStdDev(_shortBuffer); double longStdDev = CalculateStdDev(_longBuffer); - double s = _alpha * (shortStdDev / longStdDev); - vidya = (s * Input.Value) + ((1 - s) * _lastVIDYA); + vidya = CalculateVidya(shortStdDev, longStdDev); } _lastVIDYA = vidya; @@ -112,17 +133,4 @@ public class Vidya : AbstractBase return vidya; } - - /// - /// Calculates the standard deviation of values in a circular buffer. - /// - /// The circular buffer containing the values. - /// The standard deviation of the values in the buffer. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static double CalculateStdDev(CircularBuffer buffer) - { - double mean = buffer.Average(); - double sumSquaredDiff = buffer.Sum(x => Math.Pow(x - mean, 2)); - return Math.Sqrt(sumSquaredDiff / buffer.Count); - } } diff --git a/lib/averages/Wma.cs b/lib/averages/Wma.cs index b3ae1167..ce855a83 100644 --- a/lib/averages/Wma.cs +++ b/lib/averages/Wma.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -31,6 +31,7 @@ public class Wma : AbstractBase { private readonly int _period; private readonly Convolution _convolution; + private readonly double[] _kernel; /// The number of data points used in the WMA calculation. /// Thrown when period is less than 1. @@ -38,10 +39,11 @@ public class Wma : AbstractBase { if (period < 1) { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } _period = period; - _convolution = new Convolution(GenerateWmaKernel(_period)); + _kernel = GenerateWmaKernel(_period); + _convolution = new Convolution(_kernel); Name = "Wma"; WarmupPeriod = _period; Init(); @@ -60,25 +62,29 @@ public class Wma : AbstractBase /// /// The period for which to generate the kernel. /// An array of normalized linearly decreasing weights for the convolution operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double[] GenerateWmaKernel(int period) { double[] kernel = new double[period]; - double weightSum = period * (period + 1) / 2.0; + double weightSum = period * (period + 1) * 0.5; // Multiply by 0.5 instead of dividing by 2 + double invWeightSum = 1.0 / weightSum; for (int i = 0; i < period; i++) { - kernel[i] = (period - i) / weightSum; + kernel[i] = (period - i) * invWeightSum; } return kernel; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private new void Init() { base.Init(); _convolution.Init(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -93,11 +99,9 @@ public class Wma : AbstractBase ManageState(Input.IsNew); // Use Convolution for calculation - TValue convolutionResult = _convolution.Calc(Input); - - double result = convolutionResult.Value; + var convolutionResult = _convolution.Calc(Input); IsHot = _index >= WarmupPeriod; - return result; + return convolutionResult.Value; } } diff --git a/lib/averages/Zlema.cs b/lib/averages/Zlema.cs index b88374b1..cb339950 100644 --- a/lib/averages/Zlema.cs +++ b/lib/averages/Zlema.cs @@ -1,104 +1,113 @@ -using System; using System.Runtime.CompilerServices; +namespace QuanTAlib; -namespace QuanTAlib +/// +/// ZLEMA: Zero Lag Exponential Moving Average +/// A modified exponential moving average designed to reduce lag by incorporating +/// error correction based on predicted values. It estimates and removes lag by +/// extrapolating the trend using the difference between current and lagged prices. +/// +/// +/// The ZLEMA calculation process: +/// 1. Calculates lag period as (period - 1) / 2 +/// 2. Gets error correction term: 2 * price - lag_price +/// 3. Applies EMA to error-corrected price +/// 4. Results in reduced lag compared to standard EMA +/// +/// Key characteristics: +/// - Significantly reduced lag compared to EMA +/// - More responsive to price changes +/// - Uses error correction mechanism +/// - Maintains smoothness despite reduced lag +/// - Better trend following capabilities +/// +/// Sources: +/// John Ehlers and Ric Way - "Zero Lag (Well, Almost)" +/// Technical Analysis of Stocks and Commodities, 2010 +/// + +public class Zlema : AbstractBase { - /// - /// ZLEMA: Zero Lag Exponential Moving Average - /// A modified exponential moving average designed to reduce lag by incorporating - /// error correction based on predicted values. It estimates and removes lag by - /// extrapolating the trend using the difference between current and lagged prices. - /// - /// - /// The ZLEMA calculation process: - /// 1. Calculates lag period as (period - 1) / 2 - /// 2. Gets error correction term: 2 * price - lag_price - /// 3. Applies EMA to error-corrected price - /// 4. Results in reduced lag compared to standard EMA - /// - /// Key characteristics: - /// - Significantly reduced lag compared to EMA - /// - More responsive to price changes - /// - Uses error correction mechanism - /// - Maintains smoothness despite reduced lag - /// - Better trend following capabilities - /// - /// Sources: - /// John Ehlers and Ric Way - "Zero Lag (Well, Almost)" - /// Technical Analysis of Stocks and Commodities, 2010 - /// + private readonly CircularBuffer _buffer; + private readonly int _lag; + private readonly Ema _ema; + private double _lastZLEMA, _p_lastZLEMA; - public class Zlema : AbstractBase + /// The number of periods used in the ZLEMA calculation. + /// Thrown when period is less than 1. + public Zlema(int period) { - private readonly CircularBuffer _buffer; - private readonly int _lag; - private readonly Ema _ema; - private double _lastZLEMA, _p_lastZLEMA; - - /// The number of periods used in the ZLEMA calculation. - /// Thrown when period is less than 1. - public Zlema(int period) + if (period < 1) { - if (period < 1) - { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); - } - WarmupPeriod = period; - _lag = (int)(0.5 * (period - 1)); - _buffer = new CircularBuffer(_lag + 1); - _ema = new Ema(period, useSma: false); - Name = $"Zlema({period})"; - Init(); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } + WarmupPeriod = period; + _lag = (int)(0.5 * (period - 1)); + _buffer = new CircularBuffer(_lag + 1); + _ema = new Ema(period, useSma: false); + Name = $"Zlema({period})"; + Init(); + } - /// The data source object that publishes updates. - /// The number of periods used in the ZLEMA calculation. - public Zlema(object source, int period) : this(period) + /// The data source object that publishes updates. + /// The number of periods used in the ZLEMA calculation. + public Zlema(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _buffer.Clear(); + _ema.Init(); + _lastZLEMA = 0; + _p_lastZLEMA = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) { - var pubEvent = source.GetType().GetEvent("Pub"); - pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + _lastValidValue = Input.Value; + _index++; + _p_lastZLEMA = _lastZLEMA; } - - public override void Init() + else { - base.Init(); - _buffer.Clear(); - _ema.Init(); - _lastZLEMA = 0; - _p_lastZLEMA = 0; - } - - protected override void ManageState(bool isNew) - { - if (isNew) - { - _lastValidValue = Input.Value; - _index++; - _p_lastZLEMA = _lastZLEMA; - } - else - { - _lastZLEMA = _p_lastZLEMA; - } - } - - protected override double Calculation() - { - ManageState(Input.IsNew); - - _buffer.Add(Input.Value, Input.IsNew); - - // Get lagged value and calculate error correction - double lagValue = _buffer[Math.Max(0, _buffer.Count - 1 - _lag)]; - double errorCorrection = 2 * Input.Value - lagValue; - - // Apply EMA to error-corrected value - double zlema = _ema.Calc(new TValue(errorCorrection, Input.IsNew)).Value; - - _lastZLEMA = zlema; - IsHot = _index >= WarmupPeriod; - - return zlema; + _lastZLEMA = _p_lastZLEMA; } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateErrorCorrection() + { + double lagValue = _buffer[System.Math.Max(0, _buffer.Count - 1 - _lag)]; + return 2.0 * Input.Value - lagValue; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateZlema(double errorCorrection) + { + var tempValue = new TValue(Input.Time, errorCorrection, Input.IsNew); + return _ema.Calc(tempValue).Value; + } + + protected override double Calculation() + { + ManageState(Input.IsNew); + _buffer.Add(Input.Value, Input.IsNew); + + // Calculate error correction and apply EMA + double errorCorrection = CalculateErrorCorrection(); + double zlema = CalculateZlema(errorCorrection); + + _lastZLEMA = zlema; + IsHot = _index >= WarmupPeriod; + + return zlema; + } } diff --git a/lib/core/AbstractBarBase.cs b/lib/core/AbstractBarBase.cs index 3e46d8d9..1cbaf47f 100644 --- a/lib/core/AbstractBarBase.cs +++ b/lib/core/AbstractBarBase.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -10,17 +11,21 @@ namespace QuanTAlib; /// public abstract class AbstractBarBase : ITValue { - public DateTime Time { get; set; } + public System.DateTime Time { get; set; } public double Value { get; set; } public bool IsNew { get; set; } public bool IsHot { get; set; } public TBar Input { get; set; } - public String Name { get; set; } = ""; + public string Name { get; set; } = ""; public int WarmupPeriod { get; set; } + public TValue Tick => new(Time, Value, IsNew, IsHot); + public event ValueSignal Pub = delegate { }; + protected int _index; protected double _lastValidValue; + protected AbstractBarBase() { // Add parameters into constructor if needed @@ -31,17 +36,41 @@ public abstract class AbstractBarBase : ITValue /// /// The source of the bar data. /// The event arguments containing the bar data. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Sub(object source, in TBarEventArgs args) => Calc(args.Bar); /// /// Initializes the indicator's state. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual void Init() { _index = 0; _lastValidValue = 0; } + /// + /// Checks if the input value is valid (not NaN or Infinity). + /// + /// The value to check. + /// True if the value is valid, false otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected static bool IsValidValue(double value) + { + return !double.IsNaN(value) && !double.IsInfinity(value); + } + + /// + /// Creates a new TValue with the current state. + /// + /// The value to use. + /// A new TValue instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected TValue CreateTValue(double value) + { + return new TValue(Time: Input.Time, Value: value, IsNew: Input.IsNew, IsHot: IsHot); + } + /// /// Calculates the indicator value based on the input bar. /// @@ -50,21 +79,23 @@ public abstract class AbstractBarBase : ITValue public virtual TValue Calc(TBar input) { Input = input; - if (double.IsNaN(input.Close) || double.IsInfinity(input.Close)) + if (!IsValidValue(input.Close)) { - return Process(new TValue(Time: input.Time, Value: GetLastValid(), IsNew: input.IsNew, IsHot: true)); + return Process(CreateTValue(GetLastValid())); } - this.Value = Calculation(); - return Process(new TValue(Time: Input.Time, Value: this.Value, IsNew: Input.IsNew, IsHot: this.IsHot)); + + Value = Calculation(); + return Process(CreateTValue(Value)); } /// /// Retrieves the last valid calculated value. /// /// The last valid value of the indicator. + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected virtual double GetLastValid() { - return this.Value; + return Value; } /// @@ -85,12 +116,13 @@ public abstract class AbstractBarBase : ITValue /// /// The calculated TValue to process. /// The processed TValue. + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected virtual TValue Process(TValue value) { - this.Time = value.Time; - this.Value = value.Value; - this.IsNew = value.IsNew; - this.IsHot = value.IsHot; + Time = value.Time; + Value = value.Value; + IsNew = value.IsNew; + IsHot = value.IsHot; Pub?.Invoke(this, new ValueEventArgs(value)); return value; } diff --git a/lib/core/abstractBase.cs b/lib/core/abstractBase.cs index 3ac3d973..2deb5fe7 100644 --- a/lib/core/abstractBase.cs +++ b/lib/core/abstractBase.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -10,7 +11,7 @@ namespace QuanTAlib; /// public abstract class AbstractBase : ITValue { - public DateTime Time { get; set; } + public System.DateTime Time { get; set; } public double Value { get; set; } public bool IsNew { get; set; } public bool IsHot { get; set; } @@ -18,7 +19,7 @@ public abstract class AbstractBase : ITValue public TValue Input2 { get; set; } public TBar BarInput { get; set; } public TBar BarInput2 { get; set; } - public String Name { get; set; } = ""; + public string Name { get; set; } = ""; public int WarmupPeriod { get; set; } public TValue Tick => new(Time, Value, IsNew, IsHot); public event ValueSignal Pub = delegate { }; @@ -31,45 +32,64 @@ public abstract class AbstractBase : ITValue } /// - /// Subscribes to a data source and triggers calculations on new data. + /// Checks if the input value is valid (not NaN or Infinity). /// - /// The class publishing the data. - /// The argument containing the new data point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected static bool IsValidValue(double value) + { + return !double.IsNaN(value) && !double.IsInfinity(value); + } + + /// + /// Creates a new TValue with the current state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected TValue CreateTValue(System.DateTime time, double value, bool isNew, bool isHot = false) + { + return new TValue(time, value, isNew, isHot); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Sub(object source, in ValueEventArgs args) => Calc(args.Tick); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Sub(object source1, object source2, in ValueEventArgs args1, in ValueEventArgs args2) => Calc(args1.Tick, args2.Tick); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Sub(object source, in TBarEventArgs args) => Calc(args.Bar); - /// - /// Initializes the indicator's state. - /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual void Init() { _index = 0; _lastValidValue = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual TValue Calc(TValue input) { Input = input; - Input2 = new(Time: Input.Time, Value: double.NaN, IsNew: Input.IsNew, IsHot: Input.IsHot); + Input2 = CreateTValue(input.Time, double.NaN, input.IsNew, input.IsHot); return Process(input.Value, input.Time, input.IsNew); } - public virtual TValue Calc(double value, bool IsNew) + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public virtual TValue Calc(double value, bool isNew) { - Input = new(this.Time, Value: value, IsNew: IsNew, IsHot: false); - Input2 = new(this.Time, double.NaN, false, false); + Input = CreateTValue(Time, value, isNew); + Input2 = CreateTValue(Time, double.NaN, false); return Process(Input.Value, Input.Time, Input.IsNew); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual TValue Calc(TBar barInput) { BarInput = barInput; return Process(barInput.Close, barInput.Time, barInput.IsNew); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual TValue Calc(TValue input1, TValue input2) { Input = input1; @@ -77,6 +97,7 @@ public abstract class AbstractBase : ITValue return Process(input1.Value, input2.Value, input1.Time, input1.IsNew); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual TValue Calc(TBar input1, TBar input2) { BarInput = input1; @@ -84,84 +105,55 @@ public abstract class AbstractBase : ITValue return Process(input1.Close, input2.Close, input1.Time, input1.IsNew); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual TValue Calc(double value1, double value2) { - DateTime now = DateTime.Now; - Input = new TValue(now, value1, true, true); - Input2 = new TValue(now, value2, true, true); + var now = System.DateTime.Now; + Input = CreateTValue(now, value1, true, true); + Input2 = CreateTValue(now, value2, true, true); return Process(value1, value2, now, true); } - /// - /// Processes the input values, performs error checking, and calculates the indicator value. - /// - /// The primary input value to process. - /// The timestamp of the input. - /// Indicates if the input is new. - /// A TValue object with the calculated or last valid value. - protected virtual TValue Process(double value, DateTime time, bool isNew) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected virtual TValue Process(double value, System.DateTime time, bool isNew) { - if (double.IsNaN(value) || double.IsInfinity(value)) + if (!IsValidValue(value)) { - return Process(new TValue(time, GetLastValid(), isNew, this.IsHot)); + return Process(CreateTValue(time, GetLastValid(), isNew, IsHot)); } - this.Value = Calculation(); - return Process(new TValue(Time: time, Value: this.Value, IsNew: isNew, IsHot: this.IsHot)); + Value = Calculation(); + return Process(CreateTValue(time, Value, isNew, IsHot)); } - /// - /// Processes two input values, performs error checking, and calculates the indicator value. - /// - /// The first input value to process. - /// The second input value to process. - /// The timestamp of the input. - /// Indicates if the input is new. - /// A TValue object with the calculated or last valid value. - protected virtual TValue Process(double value1, double value2, DateTime time, bool isNew) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected virtual TValue Process(double value1, double value2, System.DateTime time, bool isNew) { - if (double.IsNaN(value1) || double.IsInfinity(value1) || - double.IsNaN(value2) || double.IsInfinity(value2)) + if (!IsValidValue(value1) || !IsValidValue(value2)) { - return Process(new TValue(time, GetLastValid(), isNew, this.IsHot)); + return Process(CreateTValue(time, GetLastValid(), isNew, IsHot)); } - this.Value = Calculation(); - return Process(new TValue(Time: time, Value: this.Value, IsNew: isNew, IsHot: this.IsHot)); + Value = Calculation(); + return Process(CreateTValue(time, Value, isNew, IsHot)); } - /// - /// Processes the calculated value, updates the indicator's own state, - /// and publishes the result through an event. - /// - /// The calculated TValue to process. - /// The processed TValue. + + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected virtual TValue Process(TValue value) { - this.Time = value.Time; - this.Value = value.Value; - this.IsNew = value.IsNew; - this.IsHot = value.IsHot; + Time = value.Time; + Value = value.Value; + IsNew = value.IsNew; + IsHot = value.IsHot; Pub?.Invoke(this, new ValueEventArgs(value)); return value; } - /// - /// Retrieves the last valid calculated value. - /// - /// The last valid value of the indicator. + + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected virtual double GetLastValid() { - return this.Value; + return Value; } - /// - /// Manages the state of the indicator based on whether a new data point is being processed. - /// - /// Indicates whether the current input is a new data point. protected abstract void ManageState(bool isNew); - /// - /// Performs the actual calculation of the indicator value. - /// - /// The calculated indicator value. protected abstract double Calculation(); - - } diff --git a/lib/core/circularbuffer.cs b/lib/core/circularbuffer.cs index 42740f67..a203a8e9 100644 --- a/lib/core/circularbuffer.cs +++ b/lib/core/circularbuffer.cs @@ -12,16 +12,18 @@ namespace QuanTAlib; /// a fixed-size buffer of double values. It uses SIMD operations for improved performance /// on supported hardware. /// +[SkipLocalsInit] public class CircularBuffer : IEnumerable { private readonly double[] _buffer; + private readonly int _capacity; private int _start = 0; private int _size = 0; /// /// Gets the maximum number of elements that can be contained in the buffer. /// - public int Capacity { get; } + public int Capacity => _capacity; /// /// Gets the number of elements currently contained in the buffer. @@ -32,9 +34,10 @@ public class CircularBuffer : IEnumerable /// Initializes a new instance of the CircularBuffer class with the specified capacity. /// /// The maximum number of elements the buffer can hold. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public CircularBuffer(int capacity) { - Capacity = capacity; + _capacity = capacity; _buffer = GC.AllocateArray(capacity, pinned: true); } @@ -48,20 +51,20 @@ public class CircularBuffer : IEnumerable { if (_size == 0 || isNew) { - if (_size < Capacity) + if (_size < _capacity) { - _buffer[(_start + _size) % Capacity] = item; + _buffer[(_start + _size) % _capacity] = item; _size++; } else { _buffer[_start] = item; - _start = (_start + 1) % Capacity; + _start = (_start + 1) % _capacity; } } else { - _buffer[(_start + _size - 1) % Capacity] = item; + _buffer[(_start + _size - 1) % _capacity] = item; } } @@ -77,14 +80,14 @@ public class CircularBuffer : IEnumerable { int actualIndex = index.IsFromEnd ? _size - index.Value : index.Value; actualIndex = Math.Clamp(actualIndex, 0, _size - 1); - return _buffer[(_start + actualIndex) % Capacity]; + return _buffer[(_start + actualIndex) % _capacity]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] set { int actualIndex = index.IsFromEnd ? _size - index.Value : index.Value; actualIndex = Math.Clamp(actualIndex, 0, _size - 1); - _buffer[(_start + actualIndex) % Capacity] = value; + _buffer[(_start + actualIndex) % _capacity] = value; } } @@ -103,7 +106,7 @@ public class CircularBuffer : IEnumerable { if (_size == 0) return 0; - return _buffer[(_start + _size - 1) % Capacity]; + return _buffer[(_start + _size - 1) % _capacity]; } /// @@ -128,6 +131,7 @@ public class CircularBuffer : IEnumerable /// Returns an enumerator that iterates through the buffer. /// /// An enumerator for the buffer. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Enumerator GetEnumerator() => new(this); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); @@ -135,16 +139,18 @@ public class CircularBuffer : IEnumerable /// /// Represents an enumerator for the CircularBuffer. /// - public struct Enumerator : IEnumerator + public readonly struct Enumerator : IEnumerator { private readonly CircularBuffer _buffer; - private int _index; - private double _current; + private readonly int _size; + private readonly int _index; + private readonly double _current; [MethodImpl(MethodImplOptions.AggressiveInlining)] internal Enumerator(CircularBuffer buffer) { _buffer = buffer; + _size = buffer._size; _index = -1; _current = default; } @@ -156,11 +162,11 @@ public class CircularBuffer : IEnumerable [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool MoveNext() { - if (_index + 1 >= _buffer._size) + if (_index + 1 >= _size) return false; - _index++; - _current = _buffer[_index]; + Unsafe.AsRef(in _index)++; + Unsafe.AsRef(in _current) = _buffer[_index]; return true; } @@ -173,10 +179,11 @@ public class CircularBuffer : IEnumerable /// /// Sets the enumerator to its initial position, which is before the first element in the buffer. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { - _index = -1; - _current = default; + Unsafe.AsRef(in _index) = -1; + Unsafe.AsRef(in _current) = default; } /// @@ -196,13 +203,13 @@ public class CircularBuffer : IEnumerable if (_size == 0) return; - if (_start + _size <= Capacity) + if (_start + _size <= _capacity) { Array.Copy(_buffer, _start, destination, destinationIndex, _size); } else { - int firstPartLength = Capacity - _start; + int firstPartLength = _capacity - _start; Array.Copy(_buffer, _start, destination, destinationIndex, firstPartLength); Array.Copy(_buffer, 0, destination, destinationIndex + firstPartLength, _size - firstPartLength); } @@ -218,7 +225,7 @@ public class CircularBuffer : IEnumerable if (_size == 0) return ReadOnlySpan.Empty; - if (_start + _size <= Capacity) + if (_start + _size <= _capacity) { return new ReadOnlySpan(_buffer, _start, _size); } @@ -298,7 +305,7 @@ public class CircularBuffer : IEnumerable return SumSimd() / _size; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private double MaxSimd() { var span = GetSpan(); @@ -306,9 +313,11 @@ public class CircularBuffer : IEnumerable var maxVector = new Vector(double.MinValue); int i = 0; + ref double spanRef = ref System.Runtime.InteropServices.MemoryMarshal.GetReference(span); + for (; i <= span.Length - vectorSize; i += vectorSize) { - maxVector = Vector.Max(maxVector, new Vector(span.Slice(i, vectorSize))); + maxVector = Vector.Max(maxVector, Unsafe.As>(ref Unsafe.Add(ref spanRef, i))); } double max = double.MinValue; @@ -325,7 +334,7 @@ public class CircularBuffer : IEnumerable return max; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private double MinSimd() { var span = GetSpan(); @@ -333,9 +342,11 @@ public class CircularBuffer : IEnumerable var minVector = new Vector(double.MaxValue); int i = 0; + ref double spanRef = ref System.Runtime.InteropServices.MemoryMarshal.GetReference(span); + for (; i <= span.Length - vectorSize; i += vectorSize) { - minVector = Vector.Min(minVector, new Vector(span.Slice(i, vectorSize))); + minVector = Vector.Min(minVector, Unsafe.As>(ref Unsafe.Add(ref spanRef, i))); } double min = double.MaxValue; @@ -352,7 +363,7 @@ public class CircularBuffer : IEnumerable return min; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private double SumSimd() { var span = GetSpan(); @@ -360,9 +371,11 @@ public class CircularBuffer : IEnumerable var sumVector = Vector.Zero; int i = 0; + ref double spanRef = ref System.Runtime.InteropServices.MemoryMarshal.GetReference(span); + for (; i <= span.Length - vectorSize; i += vectorSize) { - sumVector += new Vector(span.Slice(i, vectorSize)); + sumVector += Unsafe.As>(ref Unsafe.Add(ref spanRef, i)); } double sum = 0; @@ -383,6 +396,7 @@ public class CircularBuffer : IEnumerable /// Copies the buffer elements to a new array. /// /// An array containing copies of the buffer elements. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public double[] ToArray() { double[] array = new double[_size]; @@ -394,6 +408,7 @@ public class CircularBuffer : IEnumerable /// Performs a parallel operation on the buffer elements. /// /// The operation to perform on each partition of the buffer. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ParallelOperation(Func operation) { const int MinimumPartitionSize = 1024; @@ -416,7 +431,7 @@ public class CircularBuffer : IEnumerable } var buffer = ToArray(); - var results = new double[partitionCount]; + var results = GC.AllocateUninitializedArray(partitionCount); Parallel.For(0, partitionCount, i => { @@ -425,4 +440,4 @@ public class CircularBuffer : IEnumerable results[i] = operation(buffer, start, length); }); } -} \ No newline at end of file +} diff --git a/lib/core/tbar.cs b/lib/core/tbar.cs index 9b72bc88..c27682f4 100644 --- a/lib/core/tbar.cs +++ b/lib/core/tbar.cs @@ -1,3 +1,5 @@ +using System.Runtime.CompilerServices; + namespace QuanTAlib; public interface ITBar @@ -11,53 +13,74 @@ public interface ITBar bool IsNew { get; } } +[SkipLocalsInit] public readonly record struct TBar(DateTime Time, double Open, double High, double Low, double Close, double Volume, bool IsNew = true) : ITBar { public DateTime Time { get; init; } = Time; -public double Open { get; init; } = Open; -public double High { get; init; } = High; -public double Low { get; init; } = Low; -public double Close { get; init; } = Close; -public double Volume { get; init; } = Volume; -public bool IsNew { get; init; } = IsNew; + public double Open { get; init; } = Open; + public double High { get; init; } = High; + public double Low { get; init; } = Low; + public double Close { get; init; } = Close; + public double Volume { get; init; } = Volume; + public bool IsNew { get; init; } = IsNew; -public double HL2 => (High + Low) * 0.5; -public double OC2 => (Open + Close) * 0.5; -public double OHL3 => (Open + High + Low) / 3; -public double HLC3 => (High + Low + Close) / 3; -public double OHLC4 => (Open + High + Low + Close) * 0.25; -public double HLCC4 => (High + Low + Close + Close) * 0.25; + public double HL2 => (High + Low) * 0.5; + public double OC2 => (Open + Close) * 0.5; + public double OHL3 => (Open + High + Low) / 3; + public double HLC3 => (High + Low + Close) / 3; + public double OHLC4 => (Open + High + Low + Close) * 0.25; + public double HLCC4 => (High + Low + Close + Close) * 0.25; -public TBar() : this(DateTime.UtcNow, 0, 0, 0, 0, 0) { } -public TBar(double Open, double High, double Low, double Close, double Volume, bool IsNew = true) : this(DateTime.UtcNow, Open, High, Low, Close, Volume, IsNew) { } -public TBar(double value) : this(Time: DateTime.UtcNow, Open: value, High: value, Low: value, Close: value, Volume: value, IsNew: true) { } -public TBar(TValue value) : this(Time: value.Time, Open: value.Value, High: value.Value, Low: value.Value, Close: value.Value, Volume: value.Value, IsNew: value.IsNew) { } -public TBar(TBar v) : this(Time: v.Time, Open: v.Open, High: v.High, Low: v.Low, Close: v.Close, Volume: v.Volume, IsNew: true) { } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TBar() : this(DateTime.UtcNow, 0, 0, 0, 0, 0) { } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TBar(double Open, double High, double Low, double Close, double Volume, bool IsNew = true) + : this(DateTime.UtcNow, Open, High, Low, Close, Volume, IsNew) { } -public static implicit operator double(TBar bar) => bar.Close; -public static implicit operator DateTime(TBar tv) => tv.Time; -public override string ToString() => $"[{Time:yyyy-MM-dd HH:mm:ss}: O={Open:F2}, H={High:F2}, L={Low:F2}, C={Close:F2}, V={Volume:F2}]"; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TBar(double value) + : this(Time: DateTime.UtcNow, Open: value, High: value, Low: value, Close: value, Volume: value, IsNew: true) { } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TBar(TValue value) + : this(Time: value.Time, Open: value.Value, High: value.Value, Low: value.Value, Close: value.Value, Volume: value.Value, IsNew: value.IsNew) { } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TBar(TBar v) + : this(Time: v.Time, Open: v.Open, High: v.High, Low: v.Low, Close: v.Close, Volume: v.Volume, IsNew: true) { } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator double(TBar bar) => bar.Close; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator DateTime(TBar tv) => tv.Time; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override string ToString() => $"[{Time:yyyy-MM-dd HH:mm:ss}: O={Open:F2}, H={High:F2}, L={Low:F2}, C={Close:F2}, V={Volume:F2}]"; } public delegate void BarSignal(object source, in TBarEventArgs args); -public class TBarEventArgs : EventArgs +[SkipLocalsInit] +public sealed class TBarEventArgs : EventArgs { - public TBar Bar { get; } - public TBarEventArgs(TBar bar) { Bar = bar; } + public readonly TBar Bar; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TBarEventArgs(TBar bar) => Bar = bar; } +[SkipLocalsInit] public class TBarSeries : List { - private readonly TBar Default = new(DateTime.MinValue, double.NaN, double.NaN, double.NaN, double.NaN, double.NaN); - - public TSeries Open; - public TSeries High; - public TSeries Low; - public TSeries Close; - public TSeries Volume; + private static readonly TBar Default = new(DateTime.MinValue, double.NaN, double.NaN, double.NaN, double.NaN, double.NaN); + public readonly TSeries Open; + public readonly TSeries High; + public readonly TSeries Low; + public readonly TSeries Close; + public readonly TSeries Volume; public TBar Last => Count > 0 ? this[^1] : Default; public TBar First => Count > 0 ? this[0] : Default; @@ -65,22 +88,36 @@ public class TBarSeries : List public string Name { get; set; } public event BarSignal Pub = delegate { }; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public TBarSeries() { - this.Name = "Bar"; - (Open, High, Low, Close, Volume) = ([], [], [], [], []); - + Name = "Bar"; + Open = new TSeries(); + High = new TSeries(); + Low = new TSeries(); + Close = new TSeries(); + Volume = new TSeries(); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public TBarSeries(object source) : this() { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new BarSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public new virtual void Add(TBar bar) { - if (bar.IsNew || base.Count == 0) { base.Add(bar); } - else { this[^1] = bar; } + if (bar.IsNew || base.Count == 0) + { + base.Add(bar); + } + else + { + this[^1] = bar; + } + Pub?.Invoke(this, new TBarEventArgs(bar)); Open.Add(bar.Time, bar.Open, IsNew: bar.IsNew, IsHot: true); @@ -89,18 +126,22 @@ public class TBarSeries : List Close.Add(bar.Time, bar.Close, IsNew: bar.IsNew, IsHot: true); Volume.Add(bar.Time, bar.Volume, IsNew: bar.IsNew, IsHot: true); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(DateTime Time, double Open, double High, double Low, double Close, double Volume, bool IsNew = true) => - this.Add(new TBar(Time, Open, High, Low, Close, Volume, IsNew)); + Add(new TBar(Time, Open, High, Low, Close, Volume, IsNew)); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(double Open, double High, double Low, double Close, double Volume, bool IsNew = true) => - this.Add(new TBar(DateTime.Now, Open, High, Low, Close, Volume, IsNew)); + Add(new TBar(DateTime.Now, Open, High, Low, Close, Volume, IsNew)); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(TBarSeries series) { if (series == this) { // If adding itself, create a copy to avoid modification during enumeration - var copy = new TBarSeries { Name = this.Name }; + var copy = new TBarSeries { Name = Name }; copy.AddRange(this); AddRange(copy); } @@ -109,6 +150,8 @@ public class TBarSeries : List AddRange(series); } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public new virtual void AddRange(IEnumerable collection) { foreach (var item in collection) @@ -117,8 +160,9 @@ public class TBarSeries : List } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Sub(object source, in TBarEventArgs args) { Add(args.Bar); } -} \ No newline at end of file +} diff --git a/lib/core/tvalue.cs b/lib/core/tvalue.cs index 843768d6..8e54ba48 100644 --- a/lib/core/tvalue.cs +++ b/lib/core/tvalue.cs @@ -1,3 +1,5 @@ +using System.Runtime.CompilerServices; + namespace QuanTAlib; public interface ITValue @@ -8,35 +10,52 @@ public interface ITValue bool IsHot { get; } } +[SkipLocalsInit] public readonly record struct TValue(DateTime Time, double Value, bool IsNew = true, bool IsHot = true) : ITValue { public DateTime Time { get; init; } = Time; -public double Value { get; init; } = Value; -public bool IsNew { get; init; } = IsNew; -public bool IsHot { get; init; } = IsHot; -public DateTime t => Time; -public double v => Value; + public double Value { get; init; } = Value; + public bool IsNew { get; init; } = IsNew; + public bool IsHot { get; init; } = IsHot; + public DateTime t => Time; + public double v => Value; -public TValue() : this(DateTime.UtcNow, 0) { } -public TValue(double value, bool isNew = true, bool isHot = true) : this(DateTime.UtcNow, value, IsNew: isNew, IsHot: isHot) { } -public static implicit operator double(TValue tv) => tv.Value; -public static implicit operator DateTime(TValue tv) => tv.Time; -public static implicit operator TValue(double value) => new TValue(DateTime.UtcNow, value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue() : this(DateTime.UtcNow, 0) { } -public override string ToString() => $"[{Time:yyyy-MM-dd HH:mm:ss}, {Value:F2}, IsNew: {IsNew}, IsHot: {IsHot}]"; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue(double value, bool isNew = true, bool isHot = true) + : this(DateTime.UtcNow, value, IsNew: isNew, IsHot: isHot) { } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator double(TValue tv) => tv.Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator DateTime(TValue tv) => tv.Time; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator TValue(double value) => new TValue(DateTime.UtcNow, value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override string ToString() => $"[{Time:yyyy-MM-dd HH:mm:ss}, {Value:F2}, IsNew: {IsNew}, IsHot: {IsHot}]"; } public delegate void ValueSignal(object source, in ValueEventArgs args); -public class ValueEventArgs : EventArgs +[SkipLocalsInit] +public sealed class ValueEventArgs : EventArgs { - public TValue Tick { get; } - public ValueEventArgs(TValue value) { Tick = value; } + public readonly TValue Tick; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ValueEventArgs(TValue value) => Tick = value; } +[SkipLocalsInit] public class TSeries : List { - private readonly TValue Default = new(DateTime.MinValue, double.NaN); + private static readonly TValue Default = new(DateTime.MinValue, double.NaN); + public IEnumerable t => this.Select(item => item.t); public IEnumerable v => this.Select(item => item.v); public TValue Last => Count > 0 ? this[^1] : Default; @@ -45,35 +64,51 @@ public class TSeries : List public string Name { get; set; } public event ValueSignal Pub = delegate { }; - public TSeries() { this.Name = "Data"; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TSeries() + { + Name = "Data"; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public TSeries(object source) : this() { var pubEvent = source.GetType().GetEvent("Pub"); if (pubEvent != null) { -/* - var nameProperty = source.GetType().GetProperty("Name"); - if (nameProperty != null) - { - Name = nameProperty.GetValue(nameProperty)?.ToString()!; - } -*/ pubEvent.AddEventHandler(source, new ValueSignal(Sub)); } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static explicit operator List(TSeries series) => series.Select(item => item.Value).ToList(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static explicit operator double[](TSeries series) => series.Select(item => item.Value).ToArray(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public new virtual void Add(TValue tick) { - if (tick.IsNew || base.Count == 0) { base.Add(tick); } - else { this[^1] = tick; } + if (tick.IsNew || base.Count == 0) + { + base.Add(tick); + } + else + { + this[^1] = tick; + } Pub?.Invoke(this, new ValueEventArgs(tick)); } - public virtual void Add(DateTime Time, double Value, bool IsNew = true, bool IsHot = true) => this.Add(new TValue(Time, Value, IsNew, IsHot)); - public virtual void Add(double Value, bool IsNew = true, bool IsHot = true) => this.Add(new TValue(DateTime.UtcNow, Value, IsNew, IsHot)); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public virtual void Add(DateTime Time, double Value, bool IsNew = true, bool IsHot = true) => + Add(new TValue(Time, Value, IsNew, IsHot)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public virtual void Add(double Value, bool IsNew = true, bool IsHot = true) => + Add(new TValue(DateTime.UtcNow, Value, IsNew, IsHot)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(IEnumerable values) { var valueList = values.ToList(); @@ -82,16 +117,18 @@ public class TSeries : List for (int i = 0; i < count; i++) { - this.Add(startTime, valueList[i]); + Add(startTime, valueList[i]); startTime = startTime.AddHours(1); } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(TSeries series) { if (series == this) { // If adding itself, create a copy to avoid modification during enumeration - var copy = new TSeries { Name = this.Name }; + var copy = new TSeries { Name = Name }; copy.AddRange(this); AddRange(copy); } @@ -100,6 +137,8 @@ public class TSeries : List AddRange(series); } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public new virtual void AddRange(IEnumerable collection) { foreach (var item in collection) @@ -107,5 +146,7 @@ public class TSeries : List Add(item); } } - public void Sub(object source, in ValueEventArgs args) { Add(args.Tick); } -} \ No newline at end of file + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Sub(object source, in ValueEventArgs args) => Add(args.Tick); +} diff --git a/lib/errors/Huber.cs b/lib/errors/Huber.cs index 44d4915f..46bca54d 100644 --- a/lib/errors/Huber.cs +++ b/lib/errors/Huber.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -30,15 +30,18 @@ namespace QuanTAlib; /// https://projecteuclid.org/euclid.aoms/1177703732 /// -public class Huber : AbstractBase +[SkipLocalsInit] +public sealed class Huber : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; private readonly double _delta; + private readonly double _halfDelta; /// The number of points over which to calculate the loss. /// The threshold between squared and linear loss (default 1.0). /// Thrown when period is less than 1 or delta is not positive. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Huber(int period, double delta = 1.0) { if (period < 1) @@ -53,6 +56,7 @@ public class Huber : AbstractBase _actualBuffer = new CircularBuffer(period); _predictedBuffer = new CircularBuffer(period); _delta = delta; + _halfDelta = delta * 0.5; Name = $"Huberloss(period={period}, delta={delta})"; Init(); } @@ -60,12 +64,14 @@ public class Huber : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the loss. /// The threshold between squared and linear loss (default 1.0). + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Huber(object source, int period, double delta = 1.0) : this(period, delta) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -73,6 +79,7 @@ public class Huber : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -82,6 +89,20 @@ public class Huber : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private double CalculateHuberLoss(double error) + { + double absError = Math.Abs(error); + if (absError <= _delta) + { + // Squared error for small deviations + return 0.5 * error * error; + } + // Linear error for large deviations + return _delta * (absError - _halfDelta); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -96,28 +117,17 @@ public class Huber : AbstractBase double huberloss = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumLoss = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { double error = actualValues[i] - predictedValues[i]; - double absError = Math.Abs(error); - - if (absError <= _delta) - { - // Squared error for small deviations - sumLoss += 0.5 * error * error; - } - else - { - // Linear error for large deviations - sumLoss += _delta * (absError - 0.5 * _delta); - } + sumLoss += CalculateHuberLoss(error); } - huberloss = sumLoss / _actualBuffer.Count; + huberloss = sumLoss / actualValues.Length; } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Mae.cs b/lib/errors/Mae.cs index 3fc8efec..61ad8ba1 100644 --- a/lib/errors/Mae.cs +++ b/lib/errors/Mae.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -28,13 +28,15 @@ namespace QuanTAlib; /// https://www.statisticshowto.com/absolute-error/ /// -public class Mae : AbstractBase +[SkipLocalsInit] +public sealed class Mae : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the MAE. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mae(int period) { if (period < 1) @@ -50,12 +52,14 @@ public class Mae : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the MAE. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mae(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -63,6 +67,7 @@ public class Mae : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -72,6 +77,7 @@ public class Mae : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -86,16 +92,16 @@ public class Mae : AbstractBase double mae = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumAbsoluteError = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { sumAbsoluteError += Math.Abs(actualValues[i] - predictedValues[i]); } - mae = sumAbsoluteError / _actualBuffer.Count; + mae = sumAbsoluteError / actualValues.Length; } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Mapd.cs b/lib/errors/Mapd.cs index e36b4889..ad76295d 100644 --- a/lib/errors/Mapd.cs +++ b/lib/errors/Mapd.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -30,13 +30,15 @@ namespace QuanTAlib; /// Note: Also known as MAPE (Mean Absolute Percentage Error) in some contexts /// -public class Mapd : AbstractBase +[SkipLocalsInit] +public sealed class Mapd : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the MAPD. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mapd(int period) { if (period < 1) @@ -52,12 +54,14 @@ public class Mapd : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the MAPD. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mapd(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -65,6 +69,7 @@ public class Mapd : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -74,6 +79,13 @@ public class Mapd : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculatePercentageDeviation(double actual, double predicted) + { + return actual != 0 ? Math.Abs((actual - predicted) / actual) : 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -88,19 +100,16 @@ public class Mapd : AbstractBase double mapd = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumAbsolutePercentageDeviation = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { - if (actualValues[i] != 0) - { - sumAbsolutePercentageDeviation += Math.Abs((actualValues[i] - predictedValues[i]) / actualValues[i]); - } + sumAbsolutePercentageDeviation += CalculatePercentageDeviation(actualValues[i], predictedValues[i]); } - mapd = sumAbsolutePercentageDeviation / _actualBuffer.Count; + mapd = sumAbsolutePercentageDeviation / actualValues.Length; } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Mape.cs b/lib/errors/Mape.cs index 26d2492d..232aeb27 100644 --- a/lib/errors/Mape.cs +++ b/lib/errors/Mape.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -30,13 +30,15 @@ namespace QuanTAlib; /// Note: Also known as MAPD (Mean Absolute Percentage Deviation) in some contexts /// -public class Mape : AbstractBase +[SkipLocalsInit] +public sealed class Mape : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the MAPE. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mape(int period) { if (period < 1) @@ -52,12 +54,14 @@ public class Mape : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the MAPE. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mape(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -65,6 +69,7 @@ public class Mape : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -74,6 +79,13 @@ public class Mape : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculatePercentageError(double actual, double predicted) + { + return actual != 0 ? Math.Abs((actual - predicted) / actual) : 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -88,19 +100,16 @@ public class Mape : AbstractBase double mape = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumAbsolutePercentageError = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { - if (actualValues[i] != 0) - { - sumAbsolutePercentageError += Math.Abs((actualValues[i] - predictedValues[i]) / actualValues[i]); - } + sumAbsolutePercentageError += CalculatePercentageError(actualValues[i], predictedValues[i]); } - mape = sumAbsolutePercentageError / _actualBuffer.Count; + mape = sumAbsolutePercentageError / actualValues.Length; } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Mase.cs b/lib/errors/Mase.cs index 073819ec..be0fe7c1 100644 --- a/lib/errors/Mase.cs +++ b/lib/errors/Mase.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -29,7 +29,8 @@ namespace QuanTAlib; /// https://robjhyndman.com/papers/another-look-at-measures-of-forecast-accuracy/ /// -public class Mase : AbstractBase +[SkipLocalsInit] +public sealed class Mase : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; @@ -37,6 +38,7 @@ public class Mase : AbstractBase /// The number of points over which to calculate the MASE. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mase(int period) { if (period < 1) @@ -53,12 +55,14 @@ public class Mase : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the MASE. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mase(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -67,6 +71,7 @@ public class Mase : AbstractBase _naiveBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -76,6 +81,7 @@ public class Mase : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -103,6 +109,7 @@ public class Mase : AbstractBase /// Calculates the MASE value by comparing forecast error to naive forecast error. /// /// The calculated MASE value, or positive infinity if naive error is zero. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private double CalculateMase() { if (_actualBuffer.Count <= 1) return 0; @@ -112,14 +119,15 @@ public class Mase : AbstractBase ReadOnlySpan naiveValues = _naiveBuffer.GetSpan(); double sumAbsoluteError = CalculateSumAbsoluteError(actualValues, predictedValues); - double _naiveForecastError = CalculateNaiveForecastError(actualValues, naiveValues); + double naiveForecastError = CalculateNaiveForecastError(actualValues, naiveValues); - return _naiveForecastError != 0 ? (sumAbsoluteError / _actualBuffer.Count) / _naiveForecastError : double.PositiveInfinity; + return naiveForecastError != 0 ? (sumAbsoluteError / _actualBuffer.Count) / naiveForecastError : double.PositiveInfinity; } /// /// Calculates the sum of absolute errors between actual and predicted values. /// + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static double CalculateSumAbsoluteError(ReadOnlySpan actualValues, ReadOnlySpan predictedValues) { double sum = 0; @@ -133,6 +141,7 @@ public class Mase : AbstractBase /// /// Calculates the naive forecast error using the previous value as prediction. /// + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static double CalculateNaiveForecastError(ReadOnlySpan actualValues, ReadOnlySpan naiveValues) { double sum = 0; diff --git a/lib/errors/Mda.cs b/lib/errors/Mda.cs index 332337b8..cbff518a 100644 --- a/lib/errors/Mda.cs +++ b/lib/errors/Mda.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -30,13 +30,15 @@ namespace QuanTAlib; /// "Evaluating Forecasting Performance" - International Journal of Forecasting /// -public class Mda : AbstractBase +[SkipLocalsInit] +public sealed class Mda : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the MDA. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mda(int period) { if (period < 1) @@ -52,12 +54,14 @@ public class Mda : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the MDA. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mda(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -65,6 +69,7 @@ public class Mda : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -74,6 +79,13 @@ public class Mda : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static int CompareDirections(double current, double previous) + { + return Math.Sign(current - previous); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -88,18 +100,18 @@ public class Mda : AbstractBase double mda = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumDirectionalAccuracy = 0; - for (int i = 1; i < _actualBuffer.Count; i++) + for (int i = 1; i < actualValues.Length; i++) { - double actualDirection = Math.Sign(actualValues[i] - actualValues[i - 1]); - double predictedDirection = Math.Sign(predictedValues[i] - predictedValues[i - 1]); + int actualDirection = CompareDirections(actualValues[i], actualValues[i - 1]); + int predictedDirection = CompareDirections(predictedValues[i], predictedValues[i - 1]); sumDirectionalAccuracy += (actualDirection == predictedDirection) ? 1 : 0; } - mda = sumDirectionalAccuracy / (_actualBuffer.Count - 1); + mda = sumDirectionalAccuracy / (actualValues.Length - 1); } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Me.cs b/lib/errors/Me.cs index ab75705a..c4422a29 100644 --- a/lib/errors/Me.cs +++ b/lib/errors/Me.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -30,13 +30,15 @@ namespace QuanTAlib; /// Note: Also known as Mean Bias Error (MBE) or Mean Signed Difference (MSD) /// -public class Me : AbstractBase +[SkipLocalsInit] +public sealed class Me : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the ME. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Me(int period) { if (period < 1) @@ -52,12 +54,14 @@ public class Me : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the ME. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Me(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -65,6 +69,7 @@ public class Me : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -74,6 +79,13 @@ public class Me : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateError(double actual, double predicted) + { + return actual - predicted; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -88,16 +100,16 @@ public class Me : AbstractBase double me = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumError = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { - sumError += actualValues[i] - predictedValues[i]; + sumError += CalculateError(actualValues[i], predictedValues[i]); } - me = sumError / _actualBuffer.Count; + me = sumError / actualValues.Length; } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Mpe.cs b/lib/errors/Mpe.cs index cf18b88d..8ff5f825 100644 --- a/lib/errors/Mpe.cs +++ b/lib/errors/Mpe.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -31,13 +31,15 @@ namespace QuanTAlib; /// Note: Similar to MAPE but allows error cancellation /// -public class Mpe : AbstractBase +[SkipLocalsInit] +public sealed class Mpe : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the MPE. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mpe(int period) { if (period < 1) @@ -53,12 +55,14 @@ public class Mpe : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the MPE. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mpe(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -66,6 +70,7 @@ public class Mpe : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -75,6 +80,13 @@ public class Mpe : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculatePercentageError(double actual, double predicted) + { + return actual != 0 ? (actual - predicted) / actual : 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -89,19 +101,16 @@ public class Mpe : AbstractBase double mpe = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumPercentageError = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { - if (actualValues[i] != 0) - { - sumPercentageError += (actualValues[i] - predictedValues[i]) / actualValues[i]; - } + sumPercentageError += CalculatePercentageError(actualValues[i], predictedValues[i]); } - mpe = sumPercentageError / _actualBuffer.Count; + mpe = sumPercentageError / actualValues.Length; } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Mse.cs b/lib/errors/Mse.cs index 2c8fa2f6..2ca3949f 100644 --- a/lib/errors/Mse.cs +++ b/lib/errors/Mse.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -30,13 +30,15 @@ namespace QuanTAlib; /// Note: Often used in optimization due to its mathematical properties /// -public class Mse : AbstractBase +[SkipLocalsInit] +public sealed class Mse : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the MSE. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mse(int period) { if (period < 1) @@ -52,12 +54,14 @@ public class Mse : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the MSE. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mse(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -65,6 +69,7 @@ public class Mse : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -74,6 +79,14 @@ public class Mse : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateSquaredError(double actual, double predicted) + { + double error = actual - predicted; + return error * error; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -88,17 +101,16 @@ public class Mse : AbstractBase double mse = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumSquaredError = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { - double error = actualValues[i] - predictedValues[i]; - sumSquaredError += error * error; + sumSquaredError += CalculateSquaredError(actualValues[i], predictedValues[i]); } - mse = sumSquaredError / _actualBuffer.Count; + mse = sumSquaredError / actualValues.Length; } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Msle.cs b/lib/errors/Msle.cs index 27da073b..cc8bea85 100644 --- a/lib/errors/Msle.cs +++ b/lib/errors/Msle.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -31,13 +31,15 @@ namespace QuanTAlib; /// Note: Often used in cases where target values follow exponential growth /// -public class Msle : AbstractBase +[SkipLocalsInit] +public sealed class Msle : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the MSLE. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Msle(int period) { if (period < 1) @@ -53,12 +55,14 @@ public class Msle : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the MSLE. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Msle(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -66,6 +70,7 @@ public class Msle : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -75,6 +80,16 @@ public class Msle : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateSquaredLogError(double actual, double predicted) + { + double logActual = Math.Log(actual + 1); + double logPredicted = Math.Log(predicted + 1); + double error = logActual - logPredicted; + return error * error; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -89,19 +104,16 @@ public class Msle : AbstractBase double msle = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumSquaredLogError = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { - double logActual = Math.Log(actualValues[i] + 1); - double logPredicted = Math.Log(predictedValues[i] + 1); - double error = logActual - logPredicted; - sumSquaredLogError += error * error; + sumSquaredLogError += CalculateSquaredLogError(actualValues[i], predictedValues[i]); } - msle = sumSquaredLogError / _actualBuffer.Count; + msle = sumSquaredLogError / actualValues.Length; } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Rae.cs b/lib/errors/Rae.cs index a9567a02..5f5b69d1 100644 --- a/lib/errors/Rae.cs +++ b/lib/errors/Rae.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -30,13 +30,15 @@ namespace QuanTAlib; /// Note: Values greater than 1 indicate predictions worse than using zero /// -public class Rae : AbstractBase +[SkipLocalsInit] +public sealed class Rae : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the RAE. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rae(int period) { if (period < 1) @@ -52,12 +54,14 @@ public class Rae : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the RAE. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rae(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -65,6 +69,7 @@ public class Rae : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -74,6 +79,13 @@ public class Rae : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double error, double magnitude) CalculateErrorAndMagnitude(double actual, double predicted) + { + return (Math.Abs(actual - predicted), Math.Abs(actual)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -88,18 +100,19 @@ public class Rae : AbstractBase double rae = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumAbsoluteError = 0; double sumAbsoluteActual = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { - sumAbsoluteError += Math.Abs(actualValues[i] - predictedValues[i]); - sumAbsoluteActual += Math.Abs(actualValues[i]); + var (error, magnitude) = CalculateErrorAndMagnitude(actualValues[i], predictedValues[i]); + sumAbsoluteError += error; + sumAbsoluteActual += magnitude; } - rae = sumAbsoluteError / sumAbsoluteActual; + rae = sumAbsoluteActual > 0 ? sumAbsoluteError / sumAbsoluteActual : 0; } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Rmse.cs b/lib/errors/Rmse.cs index 1474fd12..04c873c0 100644 --- a/lib/errors/Rmse.cs +++ b/lib/errors/Rmse.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -31,13 +31,15 @@ namespace QuanTAlib; /// Note: Square root of MSE, making it more interpretable in original units /// -public class Rmse : AbstractBase +[SkipLocalsInit] +public sealed class Rmse : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the RMSE. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rmse(int period) { if (period < 1) @@ -53,12 +55,14 @@ public class Rmse : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the RMSE. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rmse(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -66,6 +70,7 @@ public class Rmse : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -75,6 +80,14 @@ public class Rmse : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateSquaredError(double actual, double predicted) + { + double error = actual - predicted; + return error * error; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -89,17 +102,16 @@ public class Rmse : AbstractBase double rmse = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumSquaredError = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { - double error = actualValues[i] - predictedValues[i]; - sumSquaredError += error * error; + sumSquaredError += CalculateSquaredError(actualValues[i], predictedValues[i]); } - rmse = Math.Sqrt(sumSquaredError / _actualBuffer.Count); + rmse = Math.Sqrt(sumSquaredError / actualValues.Length); } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Rmsle.cs b/lib/errors/Rmsle.cs index 846e8a65..79c66256 100644 --- a/lib/errors/Rmsle.cs +++ b/lib/errors/Rmsle.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -32,13 +32,15 @@ namespace QuanTAlib; /// Note: Square root of MSLE, useful for data with exponential growth /// -public class Rmsle : AbstractBase +[SkipLocalsInit] +public sealed class Rmsle : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the RMSLE. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rmsle(int period) { if (period < 1) @@ -54,12 +56,14 @@ public class Rmsle : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the RMSLE. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rmsle(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -67,6 +71,7 @@ public class Rmsle : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -76,6 +81,16 @@ public class Rmsle : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateSquaredLogError(double actual, double predicted) + { + double logActual = Math.Log(actual + 1); + double logPredicted = Math.Log(predicted + 1); + double error = logActual - logPredicted; + return error * error; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -90,19 +105,16 @@ public class Rmsle : AbstractBase double rmsle = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumSquaredLogError = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { - double logActual = Math.Log(actualValues[i] + 1); - double logPredicted = Math.Log(predictedValues[i] + 1); - double error = logActual - logPredicted; - sumSquaredLogError += error * error; + sumSquaredLogError += CalculateSquaredLogError(actualValues[i], predictedValues[i]); } - rmsle = Math.Sqrt(sumSquaredLogError / _actualBuffer.Count); + rmsle = Math.Sqrt(sumSquaredLogError / actualValues.Length); } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Rse.cs b/lib/errors/Rse.cs index 0074cd1b..61e87f26 100644 --- a/lib/errors/Rse.cs +++ b/lib/errors/Rse.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -31,13 +30,15 @@ namespace QuanTAlib; /// Note: Values less than 1 indicate predictions better than using mean /// -public class Rse : AbstractBase +[SkipLocalsInit] +public sealed class Rse : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the RSE. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rse(int period) { if (period < 1) @@ -53,12 +54,14 @@ public class Rse : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the RSE. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rse(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -66,6 +69,7 @@ public class Rse : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -75,6 +79,15 @@ public class Rse : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double squaredError, double squaredDeviation) CalculateErrors(double actual, double predicted, double meanActual) + { + double error = actual - predicted; + double deviation = actual - meanActual; + return (error * error, deviation * deviation); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -89,22 +102,21 @@ public class Rse : AbstractBase double rse = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumSquaredError = 0; double sumSquaredActual = 0; - double meanActual = actualValues.Average(); + double meanActual = _actualBuffer.Average(); - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { - double error = actualValues[i] - predictedValues[i]; - sumSquaredError += error * error; - double deviation = actualValues[i] - meanActual; - sumSquaredActual += deviation * deviation; + var (squaredError, squaredDeviation) = CalculateErrors(actualValues[i], predictedValues[i], meanActual); + sumSquaredError += squaredError; + sumSquaredActual += squaredDeviation; } - rse = Math.Sqrt(sumSquaredError / sumSquaredActual); + rse = sumSquaredActual > 0 ? Math.Sqrt(sumSquaredError / sumSquaredActual) : 0; } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Rsquared.cs b/lib/errors/Rsquared.cs index c7994def..f7fa5e80 100644 --- a/lib/errors/Rsquared.cs +++ b/lib/errors/Rsquared.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -31,13 +30,15 @@ namespace QuanTAlib; /// Note: Can be negative if predictions are worse than using the mean /// -public class Rsquared : AbstractBase +[SkipLocalsInit] +public sealed class Rsquared : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the R-squared value. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rsquared(int period) { if (period < 1) @@ -53,12 +54,14 @@ public class Rsquared : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the R-squared value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rsquared(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -66,6 +69,7 @@ public class Rsquared : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -75,6 +79,15 @@ public class Rsquared : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double squaredResidual, double squaredTotal) CalculateSquaredErrors(double actual, double predicted, double meanActual) + { + double deviation = actual - meanActual; + double error = actual - predicted; + return (error * error, deviation * deviation); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -89,25 +102,21 @@ public class Rsquared : AbstractBase double rsquared = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); - double meanActual = actualValues.Average(); + double meanActual = _actualBuffer.Average(); double sumSquaredTotal = 0; double sumSquaredResidual = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { - double deviation = actualValues[i] - meanActual; - sumSquaredTotal += deviation * deviation; - double error = actualValues[i] - predictedValues[i]; - sumSquaredResidual += error * error; + var (squaredResidual, squaredTotal) = CalculateSquaredErrors(actualValues[i], predictedValues[i], meanActual); + sumSquaredResidual += squaredResidual; + sumSquaredTotal += squaredTotal; } - if (sumSquaredTotal != 0) - { - rsquared = 1 - (sumSquaredResidual / sumSquaredTotal); - } + rsquared = sumSquaredTotal != 0 ? 1 - (sumSquaredResidual / sumSquaredTotal) : 0; } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Smape.cs b/lib/errors/Smape.cs index 73b47bb5..0aecc8dd 100644 --- a/lib/errors/Smape.cs +++ b/lib/errors/Smape.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -30,13 +30,16 @@ namespace QuanTAlib; /// Note: More stable than MAPE when actual values are close to zero /// -public class Smape : AbstractBase +[SkipLocalsInit] +public sealed class Smape : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; + private const double Epsilon = 1e-10; /// The number of points over which to calculate the SMAPE. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Smape(int period) { if (period < 1) @@ -52,12 +55,14 @@ public class Smape : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the SMAPE. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Smape(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -65,6 +70,7 @@ public class Smape : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -74,6 +80,14 @@ public class Smape : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateSymmetricError(double actual, double predicted) + { + double denominator = Math.Abs(actual) + Math.Abs(predicted); + return denominator > Epsilon ? Math.Abs(actual - predicted) / denominator : 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -88,18 +102,18 @@ public class Smape : AbstractBase double smape = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumSymmetricAbsolutePercentageError = 0; int validCount = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { - double denominator = Math.Abs(actualValues[i]) + Math.Abs(predictedValues[i]); - if (denominator != 0) + double error = CalculateSymmetricError(actualValues[i], predictedValues[i]); + if (error > 0) { - sumSymmetricAbsolutePercentageError += Math.Abs(actualValues[i] - predictedValues[i]) / denominator; + sumSymmetricAbsolutePercentageError += error; validCount++; } } diff --git a/lib/oscillators/Cmo.cs b/lib/oscillators/Cmo.cs index a303f67e..cd10da8c 100644 --- a/lib/oscillators/Cmo.cs +++ b/lib/oscillators/Cmo.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -34,14 +34,18 @@ namespace QuanTAlib; /// Note: Similar to RSI but with different scaling and calculation method /// -public class Cmo : AbstractBase +[SkipLocalsInit] +public sealed class Cmo : AbstractBase { private readonly CircularBuffer _sumH; private readonly CircularBuffer _sumL; private double _prevValue, _p_prevValue; + private const double Epsilon = 1e-10; + private const double ScalingFactor = 100.0; /// The number of periods used in the CMO calculation. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Cmo(int period) { if (period < 1) @@ -55,12 +59,14 @@ public class Cmo : AbstractBase /// The data source object that publishes updates. /// The number of periods used in the CMO calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Cmo(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -74,6 +80,20 @@ public class Cmo : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double up, double down) CalculateMovements(double diff) + { + return diff > 0 ? (diff, 0) : (0, -diff); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateCmo(double sumH, double sumL) + { + double divisor = sumH + sumL; + return (Math.Abs(divisor) > Epsilon) ? ScalingFactor * ((sumH - sumL) / divisor) : 0.0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -88,25 +108,11 @@ public class Cmo : AbstractBase _prevValue = Input.Value; // Separate upward and downward movements - if (diff > 0) - { - _sumH.Add(diff, Input.IsNew); - _sumL.Add(0, Input.IsNew); - } - else - { - _sumH.Add(0, Input.IsNew); - _sumL.Add(-diff, Input.IsNew); - } + var (up, down) = CalculateMovements(diff); + _sumH.Add(up, Input.IsNew); + _sumL.Add(down, Input.IsNew); - // Calculate sums for the specified period - double sumH = _sumH.Sum(); - double sumL = _sumL.Sum(); - double divisor = sumH + sumL; - - // Calculate CMO value - return (Math.Abs(divisor) > double.Epsilon) ? - 100.0 * ((sumH - sumL) / divisor) : - 0.0; + // Calculate sums and CMO value + return CalculateCmo(_sumH.Sum(), _sumL.Sum()); } } diff --git a/lib/oscillators/Rsi.cs b/lib/oscillators/Rsi.cs index 5f61fe27..324f631f 100644 --- a/lib/oscillators/Rsi.cs +++ b/lib/oscillators/Rsi.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -35,15 +35,19 @@ namespace QuanTAlib; /// Note: Default period of 14 was recommended by Wilder /// -public class Rsi : AbstractBase +[SkipLocalsInit] +public sealed class Rsi : AbstractBase { private readonly Rma _avgGain; private readonly Rma _avgLoss; private double _prevValue, _p_prevValue; + private const double ScalingFactor = 100.0; + private const int DefaultPeriod = 14; /// The number of periods used in the RSI calculation (default 14). /// Thrown when period is less than 1. - public Rsi(int period = 14) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rsi(int period = DefaultPeriod) { if (period < 1) throw new ArgumentOutOfRangeException(nameof(period)); @@ -56,12 +60,14 @@ public class Rsi : AbstractBase /// The data source object that publishes updates. /// The number of periods used in the RSI calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rsi(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -75,6 +81,19 @@ public class Rsi : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double gain, double loss) CalculateGainLoss(double change) + { + return (Math.Max(change, 0), Math.Max(-change, 0)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateRsi(double avgGain, double avgLoss) + { + return avgLoss > 0 ? ScalingFactor - (ScalingFactor / (1 + (avgGain / avgLoss))) : ScalingFactor; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -86,17 +105,14 @@ public class Rsi : AbstractBase // Calculate price change and separate gains/losses double change = Input.Value - _prevValue; - double gain = Math.Max(change, 0); - double loss = Math.Max(-change, 0); + var (gain, loss) = CalculateGainLoss(change); _prevValue = Input.Value; // Calculate smoothed averages using Wilder's method - _avgGain.Calc(gain, IsNew: Input.IsNew); - _avgLoss.Calc(loss, IsNew: Input.IsNew); + _avgGain.Calc(gain, Input.IsNew); + _avgLoss.Calc(loss, Input.IsNew); // Calculate RSI - double rsi = (_avgLoss.Value > 0) ? 100 - (100 / (1 + (_avgGain.Value / _avgLoss.Value))) : 100; - - return rsi; + return CalculateRsi(_avgGain.Value, _avgLoss.Value); } } diff --git a/lib/oscillators/Rsx.cs b/lib/oscillators/Rsx.cs index 6401c244..55d6cfd9 100644 --- a/lib/oscillators/Rsx.cs +++ b/lib/oscillators/Rsx.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -34,24 +34,34 @@ namespace QuanTAlib; /// Note: Proprietary enhancement of RSI using JMA technology /// -public class Rsx : AbstractBase +[SkipLocalsInit] +public sealed class Rsx : AbstractBase { private readonly Rma _avgGain; private readonly Rma _avgLoss; private readonly Jma _rsx; private double _prevValue, _p_prevValue; + private const double ScalingFactor = 100.0; + private const int DefaultPeriod = 14; + private const int DefaultPhase = 0; + private const double DefaultFactor = 0.55; + private const int JmaPeriod = 8; + private const int JmaPower = 100; + private const double JmaPhase = 0.25; + private const int JmaExtra = 3; /// The number of periods for RSI calculation (default 14). /// The phase parameter for JMA smoothing (default 0). /// The factor parameter for smoothing control (default 0.55). /// Thrown when period is less than 1. - public Rsx(int period = 14, int phase = 0, double factor = 0.55) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rsx(int period = DefaultPeriod, int phase = DefaultPhase, double factor = DefaultFactor) { if (period < 1) throw new ArgumentOutOfRangeException(nameof(period)); _avgGain = new(period); _avgLoss = new(period); - _rsx = new(8, 100, 0.25, 3); + _rsx = new(JmaPeriod, JmaPower, JmaPhase, JmaExtra); _index = 0; WarmupPeriod = period + 1; Name = $"RSX({period})"; @@ -61,12 +71,14 @@ public class Rsx : AbstractBase /// The number of periods for RSI calculation. /// The phase parameter for JMA smoothing. /// The factor parameter for smoothing control. - public Rsx(object source, int period, int phase = 0, double factor = 0.55) : this(period, phase, factor) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rsx(object source, int period, int phase = DefaultPhase, double factor = DefaultFactor) : this(period, phase, factor) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -80,6 +92,19 @@ public class Rsx : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double gain, double loss) CalculateGainLoss(double change) + { + return (Math.Max(change, 0), Math.Max(-change, 0)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateRsi(double avgGain, double avgLoss) + { + return avgLoss > 0 ? ScalingFactor - (ScalingFactor / (1 + (avgGain / avgLoss))) : ScalingFactor; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -91,18 +116,17 @@ public class Rsx : AbstractBase // Calculate RSI components double change = Input.Value - _prevValue; - double gain = Math.Max(change, 0); - double loss = Math.Max(-change, 0); + var (gain, loss) = CalculateGainLoss(change); _prevValue = Input.Value; // Calculate RSI - _avgGain.Calc(gain, IsNew: Input.IsNew); - _avgLoss.Calc(loss, IsNew: Input.IsNew); - double rsi = (_avgLoss.Value > 0) ? 100 - (100 / (1 + (_avgGain.Value / _avgLoss.Value))) : 100; + _avgGain.Calc(gain, Input.IsNew); + _avgLoss.Calc(loss, Input.IsNew); + double rsi = CalculateRsi(_avgGain.Value, _avgLoss.Value); // Apply JMA smoothing - double rsx = _rsx.Calc(rsi, Input.IsNew); + _rsx.Calc(rsi, Input.IsNew); - return rsx; + return _rsx.Value; } } diff --git a/lib/quantalib.csproj b/lib/quantalib.csproj index 3f706bc7..5b87dfa0 100644 --- a/lib/quantalib.csproj +++ b/lib/quantalib.csproj @@ -15,7 +15,6 @@ 0.0.0.0 True AnyCPU - False full True True diff --git a/lib/statistics/Curvature.cs b/lib/statistics/Curvature.cs index 2d51c8d3..157b5fd6 100644 --- a/lib/statistics/Curvature.cs +++ b/lib/statistics/Curvature.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -36,11 +35,13 @@ namespace QuanTAlib; /// Note: Second-order derivative providing acceleration insights /// -public class Curvature : AbstractBase +[SkipLocalsInit] +public sealed class Curvature : AbstractBase { private readonly int _period; private readonly Slope _slopeCalculator; private readonly CircularBuffer _slopeBuffer; + private const double Epsilon = 1e-10; /// /// Gets the y-intercept of the curvature line. @@ -64,6 +65,7 @@ public class Curvature : AbstractBase /// The number of points to consider for calculation. /// Thrown when period is 2 or less. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Curvature(int period) { if (period <= 2) @@ -82,12 +84,14 @@ public class Curvature : AbstractBase /// The data source object that publishes updates. /// The number of points to consider for calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Curvature(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -98,6 +102,7 @@ public class Curvature : AbstractBase Line = null; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -107,6 +112,35 @@ public class Curvature : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double sumX, double sumY) CalculateSums(ReadOnlySpan slopes, int count) + { + double sumX = 0, sumY = 0; + for (int i = 0; i < count; i++) + { + sumX += i + 1; + sumY += slopes[i]; + } + return (sumX, sumY); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double sumSqX, double sumSqY, double sumSqXY) CalculateSquaredSums( + ReadOnlySpan slopes, int count, double avgX, double avgY) + { + double sumSqX = 0, sumSqY = 0, sumSqXY = 0; + for (int i = 0; i < count; i++) + { + double devX = (i + 1) - avgX; + double devY = slopes[i] - avgY; + sumSqX += devX * devX; + sumSqY += devY * devY; + sumSqXY += devX * devY; + } + return (sumSqX, sumSqY, sumSqXY); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -122,30 +156,17 @@ public class Curvature : AbstractBase } int count = Math.Min(_slopeBuffer.Count, _period); - var slopes = _slopeBuffer.GetSpan().ToArray(); + ReadOnlySpan slopes = _slopeBuffer.GetSpan(); // Calculate averages - double sumX = 0, sumY = 0; - for (int i = 0; i < count; i++) - { - sumX += i + 1; - sumY += slopes[i]; - } + var (sumX, sumY) = CalculateSums(slopes, count); double avgX = sumX / count; double avgY = sumY / count; // Least squares method - double sumSqX = 0, sumSqY = 0, sumSqXY = 0; - for (int i = 0; i < count; i++) - { - double devX = (i + 1) - avgX; - double devY = slopes[i] - avgY; - sumSqX += devX * devX; - sumSqY += devY * devY; - sumSqXY += devX * devY; - } + var (sumSqX, sumSqY, sumSqXY) = CalculateSquaredSums(slopes, count, avgX, avgY); - if (sumSqX > 0) + if (sumSqX > Epsilon) { curvature = sumSqXY / sumSqX; Intercept = avgY - (curvature * avgX); @@ -155,9 +176,10 @@ public class Curvature : AbstractBase double stdDevY = Math.Sqrt(sumSqY / count); StdDev = stdDevY; - if (stdDevX * stdDevY != 0) + double stdDevProduct = stdDevX * stdDevY; + if (stdDevProduct > Epsilon) { - double r = sumSqXY / (stdDevX * stdDevY) / count; + double r = sumSqXY / (stdDevProduct) / count; RSquared = r * r; } diff --git a/lib/statistics/Entropy.cs b/lib/statistics/Entropy.cs index f388b183..82f15cb4 100644 --- a/lib/statistics/Entropy.cs +++ b/lib/statistics/Entropy.cs @@ -1,5 +1,5 @@ -using System; -using System.Linq; +using System.Collections.Generic; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -41,41 +41,52 @@ namespace QuanTAlib; /// Note: Normalized to [0,1] for easier interpretation /// -public class Entropy : AbstractBase +[SkipLocalsInit] +public sealed class Entropy : AbstractBase { private readonly int Period; private readonly CircularBuffer _buffer; + private readonly Dictionary _valueCounts; + private const double Epsilon = 1e-10; + private const double DefaultEntropy = 1.0; + private const int MinimumPoints = 2; /// The number of points to consider for entropy calculation. /// Thrown when period is less than 2. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Entropy(int period) { - if (period < 2) + if (period < MinimumPoints) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2 for entropy calculation."); } Period = period; - WarmupPeriod = 2; // Minimum number of points needed for entropy calculation + WarmupPeriod = MinimumPoints; // Minimum number of points needed for entropy calculation _buffer = new CircularBuffer(period); + _valueCounts = new Dictionary(); Name = $"Entropy(period={period})"; Init(); } /// The data source object that publishes updates. /// The number of points to consider for entropy calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Entropy(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _buffer.Clear(); + _valueCounts.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -85,39 +96,50 @@ public class Entropy : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static void CountValues(ReadOnlySpan values, Dictionary counts) + { + counts.Clear(); + for (int i = 0; i < values.Length; i++) + { + counts[values[i]] = counts.TryGetValue(values[i], out int count) ? count + 1 : 1; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateShannonsEntropy(Dictionary counts, int totalCount) + { + double entropy = 0; + foreach (var count in counts.Values) + { + double probability = (double)count / totalCount; + entropy -= probability * Math.Log2(probability); + } + return entropy; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); _buffer.Add(Input.Value, Input.IsNew); - double entropy = 0; - if (_index > 1) // Need at least two data points for entropy calculation + if (_index <= 1) // Need at least two data points for entropy calculation { - var values = _buffer.GetSpan().ToArray(); - int n = values.Length; - - // Calculate probabilities for each unique value - var groupedValues = values.GroupBy(x => x).Select(g => new { Value = g.Key, Count = g.Count() }); - - // Calculate Shannon's entropy - foreach (var group in groupedValues) - { - double probability = (double)group.Count / n; - entropy -= probability * Math.Log2(probability); - } - - // Normalize by maximum possible entropy for current unique values - int uniqueValueCount = groupedValues.Count(); - double maxEntropy = Math.Log2(uniqueValueCount); - - entropy = entropy == 0 ? 1 : entropy / maxEntropy; - } - else - { - entropy = 1; // Maximum entropy when insufficient data + return DefaultEntropy; } + ReadOnlySpan values = _buffer.GetSpan(); + CountValues(values, _valueCounts); + + // Calculate Shannon's entropy + double entropy = CalculateShannonsEntropy(_valueCounts, values.Length); + + // Normalize by maximum possible entropy for current unique values + double maxEntropy = Math.Log2(_valueCounts.Count); + entropy = maxEntropy < Epsilon ? DefaultEntropy : entropy / maxEntropy; + IsHot = _buffer.Count >= Period; return entropy; } diff --git a/lib/statistics/Kurtosis.cs b/lib/statistics/Kurtosis.cs index 0166b65b..023cae36 100644 --- a/lib/statistics/Kurtosis.cs +++ b/lib/statistics/Kurtosis.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -42,16 +41,20 @@ namespace QuanTAlib; /// Note: Returns excess kurtosis (normal distribution = 0) /// -public class Kurtosis : AbstractBase +[SkipLocalsInit] +public sealed class Kurtosis : AbstractBase { private readonly int Period; private readonly CircularBuffer _buffer; + private const double Epsilon = 1e-10; + private const int MinimumPoints = 4; /// The number of points to consider for kurtosis calculation. /// Thrown when period is less than 4. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Kurtosis(int period) { - if (period < 4) + if (period < MinimumPoints) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 4 for kurtosis calculation."); @@ -65,18 +68,21 @@ public class Kurtosis : AbstractBase /// The data source object that publishes updates. /// The number of points to consider for kurtosis calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Kurtosis(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _buffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -86,6 +92,48 @@ public class Kurtosis : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateMean(ReadOnlySpan values) + { + double sum = 0; + for (int i = 0; i < values.Length; i++) + { + sum += values[i]; + } + return sum / values.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double s2, double s4) CalculateDeviations(ReadOnlySpan values, double mean) + { + double s2 = 0; // Sum of squared deviations + double s4 = 0; // Sum of fourth power deviations + + for (int i = 0; i < values.Length; i++) + { + double diff = values[i] - mean; + double diff2 = diff * diff; + s2 += diff2; + s4 += diff2 * diff2; + } + + return (s2, s4); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateSheskinKurtosis(double s2, double s4, int n) + { + double variance = s2 / (n - 1); + double variance2 = variance * variance; + + if (variance2 < Epsilon) + return 0; + + return (n * (n + 1) * s4) / (variance2 * (n - 3) * (n - 1) * (n - 2)) + - (3 * (n - 1) * (n - 1) / ((n - 2) * (n - 3))); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -93,28 +141,12 @@ public class Kurtosis : AbstractBase _buffer.Add(Input.Value, Input.IsNew); double kurtosis = 0; - if (_buffer.Count > 3) // Need at least 4 points for valid calculation + if (_buffer.Count > MinimumPoints - 1) // Need at least 4 points for valid calculation { - var values = _buffer.GetSpan().ToArray(); - double mean = values.Average(); - double n = values.Length; - - // Calculate squared and fourth power deviations - double s2 = 0; // Sum of squared deviations - double s4 = 0; // Sum of fourth power deviations - - for (int i = 0; i < values.Length; i++) - { - double diff = values[i] - mean; - s2 += diff * diff; - s4 += diff * diff * diff * diff; - } - - double variance = s2 / (n - 1); - - // Sheskin Algorithm for excess kurtosis - kurtosis = (n * (n + 1) * s4) / (variance * variance * (n - 3) * (n - 1) * (n - 2)) - - (3 * (n - 1) * (n - 1) / ((n - 2) * (n - 3))); + ReadOnlySpan values = _buffer.GetSpan(); + double mean = CalculateMean(values); + var (s2, s4) = CalculateDeviations(values, mean); + kurtosis = CalculateSheskinKurtosis(s2, s4, values.Length); } IsHot = _buffer.Count >= Period; diff --git a/lib/statistics/Max.cs b/lib/statistics/Max.cs index 13ae8ad1..acbfc161 100644 --- a/lib/statistics/Max.cs +++ b/lib/statistics/Max.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -40,7 +40,8 @@ namespace QuanTAlib; /// Note: Decay factor allows for adaptive peak tracking /// -public class Max : AbstractBase +[SkipLocalsInit] +public sealed class Max : AbstractBase { private readonly int Period; private readonly CircularBuffer _buffer; @@ -49,11 +50,15 @@ public class Max : AbstractBase private double _p_currentMax; private int _timeSinceNewMax; private int _p_timeSinceNewMax; + private const double DefaultDecay = 0.0; + private const double DecayScaleFactor = 0.1; + private const double Epsilon = 1e-10; /// The number of points to consider for maximum calculation. /// Half-life decay factor (0 for no decay, higher for faster forgetting). /// Thrown when period is less than 1 or decay is negative. - public Max(int period, double decay = 0) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Max(int period, double decay = DefaultDecay) { if (period < 1) { @@ -68,7 +73,7 @@ public class Max : AbstractBase Period = period; WarmupPeriod = 0; _buffer = new CircularBuffer(period); - _halfLife = decay * 0.1; + _halfLife = decay * DecayScaleFactor; Name = $"Max(period={period}, halfLife={decay:F2})"; Init(); } @@ -76,12 +81,14 @@ public class Max : AbstractBase /// The data source object that publishes updates. /// The number of points to consider for maximum calculation. /// Half-life decay factor (default 0). - public Max(object source, int period, double decay = 0) : this(period, decay) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Max(object source, int period, double decay = DefaultDecay) : this(period, decay) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -89,6 +96,7 @@ public class Max : AbstractBase _timeSinceNewMax = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -106,6 +114,27 @@ public class Max : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private double CalculateDecayRate() + { + return 1 - Math.Exp(-_halfLife * _timeSinceNewMax / Period); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double FindMaxValue(ReadOnlySpan values) + { + double max = double.MinValue; + for (int i = 0; i < values.Length; i++) + { + if (values[i] > max) + { + max = values[i]; + } + } + return max; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -119,11 +148,12 @@ public class Max : AbstractBase } // Apply decay based on time since last maximum - double decayRate = 1 - Math.Exp(-_halfLife * _timeSinceNewMax / Period); + double decayRate = CalculateDecayRate(); _currentMax -= decayRate * (_currentMax - _buffer.Average()); // Ensure maximum doesn't exceed current period's highest value - _currentMax = Math.Min(_currentMax, _buffer.Max()); + ReadOnlySpan values = _buffer.GetSpan(); + _currentMax = Math.Min(_currentMax, FindMaxValue(values)); IsHot = true; return _currentMax; diff --git a/lib/statistics/Median.cs b/lib/statistics/Median.cs index fc203704..c8593405 100644 --- a/lib/statistics/Median.cs +++ b/lib/statistics/Median.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -40,13 +39,15 @@ namespace QuanTAlib; /// Note: More robust than mean for non-normal distributions /// -public class Median : AbstractBase +[SkipLocalsInit] +public sealed class Median : AbstractBase { private readonly int Period; private readonly CircularBuffer _buffer; /// The number of points to consider for median calculation. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Median(int period) { if (period < 1) @@ -63,18 +64,21 @@ public class Median : AbstractBase /// The data source object that publishes updates. /// The number of points to consider for median calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Median(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _buffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -84,6 +88,46 @@ public class Median : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static void QuickSort(Span arr, int left, int right) + { + if (left < right) + { + int pivotIndex = Partition(arr, left, right); + QuickSort(arr, left, pivotIndex - 1); + QuickSort(arr, pivotIndex + 1, right); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static int Partition(Span arr, int left, int right) + { + double pivot = arr[right]; + int i = left - 1; + + for (int j = left; j < right; j++) + { + if (arr[j] <= pivot) + { + i++; + (arr[i], arr[j]) = (arr[j], arr[i]); + } + } + + (arr[i + 1], arr[right]) = (arr[right], arr[i + 1]); + return i + 1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateMedian(Span sortedValues) + { + int middleIndex = sortedValues.Length / 2; + return (sortedValues.Length % 2 == 0) + ? (sortedValues[middleIndex - 1] + sortedValues[middleIndex]) / 2.0 + : sortedValues[middleIndex]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -92,15 +136,15 @@ public class Median : AbstractBase double median; if (_index >= Period) { - // Get sorted copy of values - var sortedValues = _buffer.GetSpan().ToArray(); - Array.Sort(sortedValues); - int middleIndex = sortedValues.Length / 2; + // Create a temporary buffer on the stack + Span values = stackalloc double[Period]; + _buffer.GetSpan().CopyTo(values); + + // Sort values in-place + QuickSort(values, 0, values.Length - 1); // Calculate median based on odd/even count - median = (sortedValues.Length % 2 == 0) - ? (sortedValues[middleIndex - 1] + sortedValues[middleIndex]) / 2.0 - : sortedValues[middleIndex]; + median = CalculateMedian(values); } else { diff --git a/lib/statistics/Min.cs b/lib/statistics/Min.cs index 2ebede8c..67b920eb 100644 --- a/lib/statistics/Min.cs +++ b/lib/statistics/Min.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -40,7 +40,8 @@ namespace QuanTAlib; /// Note: Decay factor allows for adaptive low tracking /// -public class Min : AbstractBase +[SkipLocalsInit] +public sealed class Min : AbstractBase { private readonly int Period; private readonly CircularBuffer _buffer; @@ -49,11 +50,15 @@ public class Min : AbstractBase private double _p_currentMin; private int _timeSinceNewMin; private int _p_timeSinceNewMin; + private const double DefaultDecay = 0.0; + private const double DecayScaleFactor = 0.1; + private const double Epsilon = 1e-10; /// The number of points to consider for minimum calculation. /// Half-life decay factor (0 for no decay, higher for faster forgetting). /// Thrown when period is less than 1 or decay is negative. - public Min(int period, double decay = 0) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Min(int period, double decay = DefaultDecay) { if (period < 1) { @@ -66,7 +71,7 @@ public class Min : AbstractBase Period = period; WarmupPeriod = 0; _buffer = new CircularBuffer(period); - _halfLife = decay * 0.1; + _halfLife = decay * DecayScaleFactor; Name = $"Min(period={period}, halfLife={decay:F2})"; Init(); } @@ -74,12 +79,14 @@ public class Min : AbstractBase /// The data source object that publishes updates. /// The number of points to consider for minimum calculation. /// Half-life decay factor (default 0). - public Min(object source, int period, double decay = 0) : this(period, decay) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Min(object source, int period, double decay = DefaultDecay) : this(period, decay) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -87,6 +94,7 @@ public class Min : AbstractBase _timeSinceNewMin = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -104,6 +112,27 @@ public class Min : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private double CalculateDecayRate() + { + return 1 - Math.Exp(-_halfLife * _timeSinceNewMin / Period); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double FindMinValue(ReadOnlySpan values) + { + double min = double.MaxValue; + for (int i = 0; i < values.Length; i++) + { + if (values[i] < min) + { + min = values[i]; + } + } + return min; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -117,11 +146,12 @@ public class Min : AbstractBase } // Apply decay based on time since last minimum - double decayRate = 1 - Math.Exp(-_halfLife * _timeSinceNewMin / Period); + double decayRate = CalculateDecayRate(); _currentMin += decayRate * (_buffer.Average() - _currentMin); // Ensure minimum doesn't fall below current period's lowest value - _currentMin = Math.Max(_currentMin, _buffer.Min()); + ReadOnlySpan values = _buffer.GetSpan(); + _currentMin = Math.Max(_currentMin, FindMinValue(values)); IsHot = true; return _currentMin; diff --git a/lib/statistics/Mode.cs b/lib/statistics/Mode.cs index 91629638..26069517 100644 --- a/lib/statistics/Mode.cs +++ b/lib/statistics/Mode.cs @@ -1,5 +1,5 @@ -using System; -using System.Linq; +using System.Collections.Generic; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -40,13 +40,18 @@ namespace QuanTAlib; /// Note: Particularly useful for price level analysis /// -public class Mode : AbstractBase +[SkipLocalsInit] +public sealed class Mode : AbstractBase { private readonly int Period; private readonly CircularBuffer _buffer; + private readonly Dictionary _frequencies; + private readonly List _modes; + private const double Epsilon = 1e-10; /// The number of points to consider for mode calculation. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mode(int period) { if (period < 1) @@ -56,24 +61,31 @@ public class Mode : AbstractBase Period = period; WarmupPeriod = period; _buffer = new CircularBuffer(period); + _frequencies = new Dictionary(); + _modes = new List(); Name = $"Mode(period={period})"; Init(); } /// The data source object that publishes updates. /// The number of points to consider for mode calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mode(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _buffer.Clear(); + _frequencies.Clear(); + _modes.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -83,6 +95,49 @@ public class Mode : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void CountFrequencies(ReadOnlySpan values) + { + _frequencies.Clear(); + for (int i = 0; i < values.Length; i++) + { + _frequencies[values[i]] = _frequencies.TryGetValue(values[i], out int count) ? count + 1 : 1; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void FindModes() + { + _modes.Clear(); + int maxCount = 0; + + foreach (var kvp in _frequencies) + { + if (kvp.Value > maxCount) + { + maxCount = kvp.Value; + _modes.Clear(); + _modes.Add(kvp.Key); + } + else if (kvp.Value == maxCount) + { + _modes.Add(kvp.Key); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private double CalculateAverageMode() + { + double sum = 0; + for (int i = 0; i < _modes.Count; i++) + { + sum += _modes[i]; + } + return sum / _modes.Count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -91,21 +146,10 @@ public class Mode : AbstractBase double mode; if (_index >= Period) { - // Group values by frequency and order by count - var values = _buffer.GetSpan().ToArray(); - var groupedValues = values.GroupBy(v => v) - .OrderByDescending(g => g.Count()) - .ThenBy(g => g.Key) - .ToList(); - - // Find all values with highest frequency - int maxCount = groupedValues.First().Count(); - var modes = groupedValues.TakeWhile(g => g.Count() == maxCount) - .Select(g => g.Key) - .ToList(); - - // Average multiple modes if present - mode = modes.Average(); + ReadOnlySpan values = _buffer.GetSpan(); + CountFrequencies(values); + FindModes(); + mode = CalculateAverageMode(); } else { diff --git a/lib/statistics/Percentile.cs b/lib/statistics/Percentile.cs index 4978a6ae..4617cdc0 100644 --- a/lib/statistics/Percentile.cs +++ b/lib/statistics/Percentile.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -41,20 +40,24 @@ namespace QuanTAlib; /// Note: Particularly useful for risk metrics like VaR /// -public class Percentile : AbstractBase +[SkipLocalsInit] +public sealed class Percentile : AbstractBase { private readonly int Period; private readonly double Percent; private readonly CircularBuffer _buffer; + private const double Epsilon = 1e-10; + private const int MinimumPoints = 2; /// The number of points to consider for percentile calculation. /// The percentile to calculate (0-100). /// /// Thrown when period is less than 2 or percent is not between 0 and 100. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Percentile(int period, double percent) { - if (period < 2) + if (period < MinimumPoints) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2 for percentile calculation."); @@ -66,7 +69,7 @@ public class Percentile : AbstractBase } Period = period; Percent = percent; - WarmupPeriod = 2; // Minimum number of points needed for percentile calculation + WarmupPeriod = MinimumPoints; // Minimum number of points needed for percentile calculation _buffer = new CircularBuffer(period); Name = $"Percentile(period={period}, percent={percent})"; Init(); @@ -75,18 +78,21 @@ public class Percentile : AbstractBase /// The data source object that publishes updates. /// The number of points to consider for percentile calculation. /// The percentile to calculate (0-100). + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Percentile(object source, int period, double percent) : this(period, percent) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _buffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -96,6 +102,56 @@ public class Percentile : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static void QuickSort(Span arr, int left, int right) + { + if (left < right) + { + int pivotIndex = Partition(arr, left, right); + QuickSort(arr, left, pivotIndex - 1); + QuickSort(arr, pivotIndex + 1, right); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static int Partition(Span arr, int left, int right) + { + double pivot = arr[right]; + int i = left - 1; + + for (int j = left; j < right; j++) + { + if (arr[j] <= pivot) + { + i++; + (arr[i], arr[j]) = (arr[j], arr[i]); + } + } + + (arr[i + 1], arr[right]) = (arr[right], arr[i + 1]); + return i + 1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private double CalculatePercentile(Span sortedValues) + { + double position = (Percent / 100.0) * (sortedValues.Length - 1); + int lowerIndex = (int)Math.Floor(position); + int upperIndex = (int)Math.Ceiling(position); + + if (lowerIndex == upperIndex) + { + return sortedValues[lowerIndex]; + } + + // Linear interpolation between adjacent values + double lowerValue = sortedValues[lowerIndex]; + double upperValue = sortedValues[upperIndex]; + double fraction = position - lowerIndex; + return lowerValue + (upperValue - lowerValue) * fraction; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -104,26 +160,12 @@ public class Percentile : AbstractBase double result; if (_buffer.Count >= Period) { - // Sort values and calculate percentile position - var values = _buffer.GetSpan().ToArray(); - Array.Sort(values); + // Create a temporary buffer on the stack and sort values + Span values = stackalloc double[Period]; + _buffer.GetSpan().CopyTo(values); + QuickSort(values, 0, values.Length - 1); - double position = (Percent / 100.0) * (values.Length - 1); - int lowerIndex = (int)Math.Floor(position); - int upperIndex = (int)Math.Ceiling(position); - - if (lowerIndex == upperIndex) - { - result = values[lowerIndex]; - } - else - { - // Linear interpolation between adjacent values - double lowerValue = values[lowerIndex]; - double upperValue = values[upperIndex]; - double fraction = position - lowerIndex; - result = lowerValue + (upperValue - lowerValue) * fraction; - } + result = CalculatePercentile(values); } else { diff --git a/lib/statistics/Skew.cs b/lib/statistics/Skew.cs index 3ab7db22..65f9613f 100644 --- a/lib/statistics/Skew.cs +++ b/lib/statistics/Skew.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -44,22 +43,26 @@ namespace QuanTAlib; /// Note: Requires minimum of 3 data points for calculation /// -public class Skew : AbstractBase +[SkipLocalsInit] +public sealed class Skew : AbstractBase { private readonly int Period; private readonly CircularBuffer _buffer; + private const double Epsilon = 1e-10; + private const int MinimumPoints = 3; /// The number of points to consider for skewness calculation. /// Thrown when period is less than 3. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Skew(int period) { - if (period < 3) + if (period < MinimumPoints) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 3 for skewness calculation."); } Period = period; - WarmupPeriod = 3; + WarmupPeriod = MinimumPoints; _buffer = new CircularBuffer(period); Name = $"Skew(period={period})"; Init(); @@ -67,18 +70,21 @@ public class Skew : AbstractBase /// The data source object that publishes updates. /// The number of points to consider for skewness calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Skew(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _buffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -88,38 +94,58 @@ public class Skew : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateMean(ReadOnlySpan values) + { + double sum = 0; + for (int i = 0; i < values.Length; i++) + { + sum += values[i]; + } + return sum / values.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double m3, double m2) CalculateMoments(ReadOnlySpan values, double mean) + { + double sumCubedDeviations = 0; + double sumSquaredDeviations = 0; + + for (int i = 0; i < values.Length; i++) + { + double deviation = values[i] - mean; + double squared = deviation * deviation; + sumSquaredDeviations += squared; + sumCubedDeviations += squared * deviation; + } + + double n = values.Length; + return (sumCubedDeviations / n, sumSquaredDeviations / n); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateSkewness(double m3, double m2, int n) + { + double s3 = Math.Pow(m2, 1.5); + if (s3 < Epsilon) + return 0; + + return (Math.Sqrt(n * (n - 1)) / (n - 2)) * (m3 / s3); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); _buffer.Add(Input.Value, Input.IsNew); double skew = 0; - if (_buffer.Count >= 3) // Need at least 3 points for skewness + if (_buffer.Count >= MinimumPoints) // Need at least 3 points for skewness { - var values = _buffer.GetSpan().ToArray(); - double mean = values.Average(); - double n = values.Length; - - // Calculate third and second moments - double sumCubedDeviations = 0; - double sumSquaredDeviations = 0; - - foreach (var value in values) - { - double deviation = value - mean; - sumCubedDeviations += Math.Pow(deviation, 3); - sumSquaredDeviations += Math.Pow(deviation, 2); - } - - // Fisher-Pearson standardized moment coefficient - double m3 = sumCubedDeviations / n; - double m2 = sumSquaredDeviations / n; - double s3 = Math.Pow(m2, 1.5); - - if (s3 != 0) // Avoid division by zero - { - skew = (Math.Sqrt(n * (n - 1)) / (n - 2)) * (m3 / s3); - } + ReadOnlySpan values = _buffer.GetSpan(); + double mean = CalculateMean(values); + var (m3, m2) = CalculateMoments(values, mean); + skew = CalculateSkewness(m3, m2, values.Length); } IsHot = _buffer.Count >= Period; diff --git a/lib/statistics/Slope.cs b/lib/statistics/Slope.cs index 8731e507..df9adf15 100644 --- a/lib/statistics/Slope.cs +++ b/lib/statistics/Slope.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -43,11 +42,14 @@ namespace QuanTAlib; /// Note: Provides additional regression statistics (R², intercept) /// -public class Slope : AbstractBase +[SkipLocalsInit] +public sealed class Slope : AbstractBase { private readonly int _period; private readonly CircularBuffer _buffer; private readonly CircularBuffer _timeBuffer; + private const double Epsilon = 1e-10; + private const int MinimumPoints = 2; /// Gets the y-intercept of the regression line. public double? Intercept { get; private set; } @@ -63,6 +65,7 @@ public class Slope : AbstractBase /// The number of points to consider for slope calculation. /// Thrown when period is less than or equal to 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Slope(int period) { if (period <= 1) @@ -80,12 +83,14 @@ public class Slope : AbstractBase /// The data source object that publishes updates. /// The number of points to consider for slope calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Slope(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -97,6 +102,7 @@ public class Slope : AbstractBase Line = null; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -106,33 +112,22 @@ public class Slope : AbstractBase } } - protected override double Calculation() + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double sumX, double sumY) CalculateSums(ReadOnlySpan values, int count) { - ManageState(Input.IsNew); - - _buffer.Add(Input.Value, Input.IsNew); - _timeBuffer.Add(Input.Time.Ticks, Input.IsNew); - - double slope = 0; - if (_buffer.Count < 2) - { - return slope; // Need at least 2 points - } - - int count = Math.Min(_buffer.Count, _period); - var values = _buffer.GetSpan().ToArray(); - - // Calculate averages double sumX = 0, sumY = 0; for (int i = 0; i < count; i++) { sumX += i + 1; sumY += values[i]; } - double avgX = sumX / count; - double avgY = sumY / count; + return (sumX, sumY); + } - // Least squares regression + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double sumSqX, double sumSqY, double sumSqXY) CalculateSquaredSums( + ReadOnlySpan values, int count, double avgX, double avgY) + { double sumSqX = 0, sumSqY = 0, sumSqXY = 0; for (int i = 0; i < count; i++) { @@ -142,8 +137,35 @@ public class Slope : AbstractBase sumSqY += devY * devY; sumSqXY += devX * devY; } + return (sumSqX, sumSqY, sumSqXY); + } - if (sumSqX > 0) + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + _buffer.Add(Input.Value, Input.IsNew); + _timeBuffer.Add(Input.Time.Ticks, Input.IsNew); + + double slope = 0; + if (_buffer.Count < MinimumPoints) + { + return slope; // Need at least 2 points + } + + int count = Math.Min(_buffer.Count, _period); + ReadOnlySpan values = _buffer.GetSpan(); + + // Calculate averages + var (sumX, sumY) = CalculateSums(values, count); + double avgX = sumX / count; + double avgY = sumY / count; + + // Least squares regression + var (sumSqX, sumSqY, sumSqXY) = CalculateSquaredSums(values, count, avgX, avgY); + + if (sumSqX > Epsilon) { // Calculate slope and related statistics slope = sumSqXY / sumSqX; @@ -154,9 +176,10 @@ public class Slope : AbstractBase double stdDevY = Math.Sqrt(sumSqY / count); StdDev = stdDevY; - if (stdDevX * stdDevY != 0) + double stdDevProduct = stdDevX * stdDevY; + if (stdDevProduct > Epsilon) { - double r = sumSqXY / (stdDevX * stdDevY) / count; + double r = sumSqXY / stdDevProduct / count; RSquared = r * r; } diff --git a/lib/statistics/Stddev.cs b/lib/statistics/Stddev.cs index 47433a91..4504a58f 100644 --- a/lib/statistics/Stddev.cs +++ b/lib/statistics/Stddev.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -44,17 +43,21 @@ namespace QuanTAlib; /// Note: Foundation for many volatility-based indicators /// -public class Stddev : AbstractBase +[SkipLocalsInit] +public sealed class Stddev : AbstractBase { private readonly bool IsPopulation; private readonly CircularBuffer _buffer; + private const double Epsilon = 1e-10; + private const int MinimumPoints = 2; /// The number of points to consider for standard deviation calculation. /// True for population stddev, false for sample stddev (default). /// Thrown when period is less than 2. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Stddev(int period, bool isPopulation = false) { - if (period < 2) + if (period < MinimumPoints) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2."); @@ -69,18 +72,21 @@ public class Stddev : AbstractBase /// The data source object that publishes updates. /// The number of points to consider for standard deviation calculation. /// True for population stddev, false for sample stddev (default). + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Stddev(object source, int period, bool isPopulation = false) : this(period, isPopulation) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _buffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -90,6 +96,30 @@ public class Stddev : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateMean(ReadOnlySpan values) + { + double sum = 0; + for (int i = 0; i < values.Length; i++) + { + sum += values[i]; + } + return sum / values.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateSumSquaredDeviations(ReadOnlySpan values, double mean) + { + double sum = 0; + for (int i = 0; i < values.Length; i++) + { + double diff = values[i] - mean; + sum += diff * diff; + } + return sum; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -98,11 +128,9 @@ public class Stddev : AbstractBase double stddev = 0; if (_buffer.Count > 1) { - var values = _buffer.GetSpan().ToArray(); - double mean = values.Average(); - - // Calculate sum of squared deviations - double sumOfSquaredDifferences = values.Sum(x => Math.Pow(x - mean, 2)); + ReadOnlySpan values = _buffer.GetSpan(); + double mean = CalculateMean(values); + double sumOfSquaredDifferences = CalculateSumSquaredDeviations(values, mean); // Use appropriate divisor based on population/sample calculation double divisor = IsPopulation ? _buffer.Count : _buffer.Count - 1; diff --git a/lib/statistics/Variance.cs b/lib/statistics/Variance.cs index 12916507..a7c30a9f 100644 --- a/lib/statistics/Variance.cs +++ b/lib/statistics/Variance.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -44,17 +43,21 @@ namespace QuanTAlib; /// Note: Basis for Modern Portfolio Theory and risk models /// -public class Variance : AbstractBase +[SkipLocalsInit] +public sealed class Variance : AbstractBase { private readonly bool IsPopulation; private readonly CircularBuffer _buffer; + private const double Epsilon = 1e-10; + private const int MinimumPoints = 2; /// The number of points to consider for variance calculation. /// True for population variance, false for sample variance (default). /// Thrown when period is less than 2. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Variance(int period, bool isPopulation = false) { - if (period < 2) + if (period < MinimumPoints) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2."); @@ -69,18 +72,21 @@ public class Variance : AbstractBase /// The data source object that publishes updates. /// The number of points to consider for variance calculation. /// True for population variance, false for sample variance (default). + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Variance(object source, int period, bool isPopulation = false) : this(period, isPopulation) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _buffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -90,6 +96,30 @@ public class Variance : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateMean(ReadOnlySpan values) + { + double sum = 0; + for (int i = 0; i < values.Length; i++) + { + sum += values[i]; + } + return sum / values.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateSumSquaredDeviations(ReadOnlySpan values, double mean) + { + double sum = 0; + for (int i = 0; i < values.Length; i++) + { + double diff = values[i] - mean; + sum += diff * diff; + } + return sum; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -98,11 +128,9 @@ public class Variance : AbstractBase double variance = 0; if (_buffer.Count > 1) { - var values = _buffer.GetSpan().ToArray(); - double mean = values.Average(); - - // Calculate sum of squared deviations - double sumOfSquaredDifferences = values.Sum(x => Math.Pow(x - mean, 2)); + ReadOnlySpan values = _buffer.GetSpan(); + double mean = CalculateMean(values); + double sumOfSquaredDifferences = CalculateSumSquaredDeviations(values, mean); // Use appropriate divisor based on population/sample calculation double divisor = IsPopulation ? _buffer.Count : _buffer.Count - 1; diff --git a/lib/statistics/Zscore.cs b/lib/statistics/Zscore.cs index c3c72769..752559fb 100644 --- a/lib/statistics/Zscore.cs +++ b/lib/statistics/Zscore.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -43,22 +42,26 @@ namespace QuanTAlib; /// Note: Assumes approximately normal distribution /// -public class Zscore : AbstractBase +[SkipLocalsInit] +public sealed class Zscore : AbstractBase { private readonly int Period; private readonly CircularBuffer _buffer; + private const double Epsilon = 1e-10; + private const int MinimumPoints = 2; /// The number of points to consider for Z-score calculation. /// Thrown when period is less than 2. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Zscore(int period) { - if (period < 2) + if (period < MinimumPoints) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2 for Z-score calculation."); } Period = period; - WarmupPeriod = 2; + WarmupPeriod = MinimumPoints; _buffer = new CircularBuffer(period); Name = $"ZScore(period={period})"; Init(); @@ -66,18 +69,21 @@ public class Zscore : AbstractBase /// The data source object that publishes updates. /// The number of points to consider for Z-score calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Zscore(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _buffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -87,23 +93,43 @@ public class Zscore : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateMean(ReadOnlySpan values) + { + double sum = 0; + for (int i = 0; i < values.Length; i++) + { + sum += values[i]; + } + return sum / values.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateStandardDeviation(ReadOnlySpan values, double mean) + { + double sumSquaredDeviations = 0; + for (int i = 0; i < values.Length; i++) + { + double deviation = values[i] - mean; + sumSquaredDeviations += deviation * deviation; + } + return Math.Sqrt(sumSquaredDeviations / (values.Length - 1)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); _buffer.Add(Input.Value, Input.IsNew); double zScore = 0; - if (_buffer.Count >= 2) // Need at least 2 points for standard deviation + if (_buffer.Count >= MinimumPoints) // Need at least 2 points for standard deviation { - var values = _buffer.GetSpan().ToArray(); - double mean = values.Average(); - double n = values.Length; + ReadOnlySpan values = _buffer.GetSpan(); + double mean = CalculateMean(values); + double standardDeviation = CalculateStandardDeviation(values, mean); - // Calculate sample standard deviation - double sumSquaredDeviations = values.Sum(x => Math.Pow(x - mean, 2)); - double standardDeviation = Math.Sqrt(sumSquaredDeviations / (n - 1)); - - if (standardDeviation != 0) // Avoid division by zero + if (standardDeviation > Epsilon) // Avoid division by zero { zScore = (Input.Value - mean) / standardDeviation; } diff --git a/lib/volatility/Atr.cs b/lib/volatility/Atr.cs index 43d1983a..c098e83f 100644 --- a/lib/volatility/Atr.cs +++ b/lib/volatility/Atr.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -42,7 +42,8 @@ namespace QuanTAlib; /// Note: Higher ATR indicates higher volatility /// -public class Atr : AbstractBase +[SkipLocalsInit] +public sealed class Atr : AbstractBase { public double Tr { get; private set; } private readonly Rma _ma; @@ -50,6 +51,7 @@ public class Atr : AbstractBase /// The number of periods for ATR calculation. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Atr(int period) { if (period < 1) @@ -64,12 +66,14 @@ public class Atr : AbstractBase /// The data source object that publishes updates. /// The number of periods for ATR calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Atr(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new BarSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -78,6 +82,7 @@ public class Atr : AbstractBase Tr = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -91,6 +96,17 @@ public class Atr : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateTrueRange(double high, double low, double prevClose) + { + double highLowRange = high - low; + double highPrevCloseRange = Math.Abs(high - prevClose); + double lowPrevCloseRange = Math.Abs(low - prevClose); + + return Math.Max(highLowRange, Math.Max(highPrevCloseRange, lowPrevCloseRange)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(BarInput.IsNew); @@ -104,13 +120,7 @@ public class Atr : AbstractBase else { // Calculate True Range as maximum of three measures - Tr = Math.Max( - BarInput.High - BarInput.Low, - Math.Max( - Math.Abs(BarInput.High - _prevClose), - Math.Abs(BarInput.Low - _prevClose) - ) - ); + Tr = CalculateTrueRange(BarInput.High, BarInput.Low, _prevClose); } // Apply RMA smoothing to True Range diff --git a/lib/volatility/Hv.cs b/lib/volatility/Hv.cs index f6c73cc7..fb246cce 100644 --- a/lib/volatility/Hv.cs +++ b/lib/volatility/Hv.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -44,17 +43,21 @@ namespace QuanTAlib; /// Note: Assumes 252 trading days for annualization /// -public class Hv : AbstractBase +[SkipLocalsInit] +public sealed class Hv : AbstractBase { private readonly int Period; private readonly bool IsAnnualized; private readonly CircularBuffer _buffer; private readonly CircularBuffer _logReturns; private double _previousClose; + private const int TradingDaysPerYear = 252; + private const double Epsilon = 1e-10; /// The number of periods for volatility calculation. /// Whether to annualize the result (default true). /// Thrown when period is less than 2. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Hv(int period, bool isAnnualized = true) { if (period < 2) @@ -74,12 +77,14 @@ public class Hv : AbstractBase /// The data source object that publishes updates. /// The number of periods for volatility calculation. /// Whether to annualize the result (default true). + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Hv(object source, int period, bool isAnnualized = true) : this(period, isAnnualized) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -88,6 +93,7 @@ public class Hv : AbstractBase _previousClose = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -97,6 +103,36 @@ public class Hv : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateLogReturn(double currentPrice, double previousPrice) + { + return previousPrice > Epsilon ? Math.Log(currentPrice / previousPrice) : 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateMean(ReadOnlySpan values) + { + double sum = 0; + for (int i = 0; i < values.Length; i++) + { + sum += values[i]; + } + return sum / values.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateVariance(ReadOnlySpan values, double mean, int degreesOfFreedom) + { + double sumSquaredDiff = 0; + for (int i = 0; i < values.Length; i++) + { + double diff = values[i] - mean; + sumSquaredDiff += diff * diff; + } + return sumSquaredDiff / degreesOfFreedom; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -106,26 +142,23 @@ public class Hv : AbstractBase if (_buffer.Count > 1) { // Calculate log return if we have previous close - if (_previousClose != 0) + if (_previousClose > Epsilon) { - double logReturn = Math.Log(Input.Value / _previousClose); + double logReturn = CalculateLogReturn(Input.Value, _previousClose); _logReturns.Add(logReturn, Input.IsNew); } // Calculate volatility when we have enough returns if (_logReturns.Count == Period) { - var returns = _logReturns.GetSpan().ToArray(); - double mean = returns.Average(); - double sumOfSquaredDifferences = returns.Sum(x => Math.Pow(x - mean, 2)); - - // Sample standard deviation - double variance = sumOfSquaredDifferences / (Period - 1); + ReadOnlySpan returns = _logReturns.GetSpan(); + double mean = CalculateMean(returns); + double variance = CalculateVariance(returns, mean, Period - 1); volatility = Math.Sqrt(variance); if (IsAnnualized) { - volatility *= Math.Sqrt(252); // Annualize using trading days + volatility *= Math.Sqrt(TradingDaysPerYear); } } } diff --git a/lib/volatility/Jvolty.cs b/lib/volatility/Jvolty.cs index af7c305e..424bc8cb 100644 --- a/lib/volatility/Jvolty.cs +++ b/lib/volatility/Jvolty.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -41,31 +41,37 @@ namespace QuanTAlib; /// Note: Proprietary enhancement of volatility measurement /// -public class Jvolty : AbstractBase +[SkipLocalsInit] +public sealed class Jvolty : AbstractBase { private readonly int _period; private readonly double _phase; private readonly CircularBuffer _vsumBuff; private readonly CircularBuffer _avoltyBuff; + private readonly double _beta; + private const double Epsilon = 1e-10; + private const int DefaultPhase = 0; + private const int VsumBufferSize = 10; + private const int AvoltyBufferSize = 65; private double _len1; private double _pow1; - private readonly double _beta; private double _upperBand, _lowerBand, _p_upperBand, _p_lowerBand; private double _prevMa1, _prevDet0, _prevDet1, _prevJma, _p_prevMa1, _p_prevDet0, _p_prevDet1, _p_prevJma; private double _vSum, _p_vSum; - public double UpperBand { get; set; } - public double LowerBand { get; set; } - public double Volty { get; set; } - public double VSum { get; set; } - public double Jma { get; set; } - public double AvgVolty { get; set; } + public double UpperBand { get; private set; } + public double LowerBand { get; private set; } + public double Volty { get; private set; } + public double VSum { get; private set; } + public double Jma { get; private set; } + public double AvgVolty { get; private set; } /// The number of periods for volatility calculation. /// Phase parameter for JMA smoothing (default 0). /// Thrown when period is less than 1. - public Jvolty(int period, int phase = 0) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Jvolty(int period, int phase = DefaultPhase) { if (period < 1) { @@ -75,8 +81,8 @@ public class Jvolty : AbstractBase _period = period; _phase = Math.Clamp((phase * 0.01) + 1.5, 0.5, 2.5); - _vsumBuff = new CircularBuffer(10); - _avoltyBuff = new CircularBuffer(65); + _vsumBuff = new CircularBuffer(VsumBufferSize); + _avoltyBuff = new CircularBuffer(AvoltyBufferSize); _beta = 0.45 * (period - 1) / (0.45 * (period - 1) + 2); WarmupPeriod = period * 2; @@ -86,12 +92,14 @@ public class Jvolty : AbstractBase /// The data source object that publishes updates. /// The number of periods for volatility calculation. /// Phase parameter for JMA smoothing (default 0). - public Jvolty(object source, int period, int phase = 0) : this(period, phase) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Jvolty(object source, int period, int phase = DefaultPhase) : this(period, phase) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new BarSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -103,6 +111,7 @@ public class Jvolty : AbstractBase _vsumBuff.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -128,6 +137,37 @@ public class Jvolty : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateVolatility(double price, double upperBand, double lowerBand) + { + double del1 = price - upperBand; + double del2 = price - lowerBand; + return Math.Max(Math.Abs(del1), Math.Abs(del2)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private double CalculateNormalizedVolatility(double volty, double avgVolty) + { + double rvolty = (avgVolty > Epsilon) ? volty / avgVolty : 1; + return Math.Min(Math.Max(rvolty, 1.0), Math.Pow(_len1, 1.0 / _pow1)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private double CalculateJma(double price, double alpha, double ma1) + { + double det0 = (price - ma1) * (1 - _beta) + _beta * _prevDet0; + _prevDet0 = det0; + double ma2 = ma1 + _phase * det0; + + double det1 = ((ma2 - _prevJma) * (1 - alpha) * (1 - alpha)) + (alpha * alpha * _prevDet1); + _prevDet1 = det1; + double jma = _prevJma + det1; + _prevJma = jma; + + return jma; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -139,40 +179,31 @@ public class Jvolty : AbstractBase } // Calculate volatility from band distances - double del1 = price - _upperBand; - double del2 = price - _lowerBand; - double volty = Math.Max(Math.Abs(del1), Math.Abs(del2)); + double volty = CalculateVolatility(price, _upperBand, _lowerBand); // Calculate moving averages of volatility _vsumBuff.Add(volty, Input.IsNew); - _vSum += (_vsumBuff[^1] - _vsumBuff[0]) / 10; + _vSum += (_vsumBuff[^1] - _vsumBuff[0]) / VsumBufferSize; _avoltyBuff.Add(_vSum, Input.IsNew); double avgvolty = _avoltyBuff.Average(); // Normalize and adjust volatility - double rvolty = (avgvolty > 0) ? volty / avgvolty : 1; - rvolty = Math.Min(Math.Max(rvolty, 1.0), Math.Pow(_len1, 1.0 / _pow1)); - + double rvolty = CalculateNormalizedVolatility(volty, avgvolty); double pow2 = Math.Pow(rvolty, _pow1); double Kv = Math.Pow(_beta, Math.Sqrt(pow2)); // Update adaptive bands + double del1 = price - _upperBand; + double del2 = price - _lowerBand; _upperBand = (del1 >= 0) ? price : price - (Kv * del1); _lowerBand = (del2 <= 0) ? price : price - (Kv * del2); // Apply JMA smoothing double alpha = Math.Pow(_beta, pow2); - double ma1 = (1 - alpha) * Input.Value + alpha * _prevMa1; + double ma1 = (1 - alpha) * price + alpha * _prevMa1; _prevMa1 = ma1; - double det0 = (price - ma1) * (1 - _beta) + _beta * _prevDet0; - _prevDet0 = det0; - double ma2 = ma1 + _phase * det0; - - double det1 = ((ma2 - _prevJma) * (1 - alpha) * (1 - alpha)) + (alpha * alpha * _prevDet1); - _prevDet1 = det1; - double jma = _prevJma + det1; - _prevJma = jma; + double jma = CalculateJma(price, alpha, ma1); // Update public properties UpperBand = _upperBand; diff --git a/lib/volatility/Rv.cs b/lib/volatility/Rv.cs index b171a173..d53615c4 100644 --- a/lib/volatility/Rv.cs +++ b/lib/volatility/Rv.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -43,18 +43,23 @@ namespace QuanTAlib; /// Note: Efficient implementation using rolling sums /// -public class Rv : AbstractBase +[SkipLocalsInit] +public sealed class Rv : AbstractBase { private readonly int Period; private readonly bool IsAnnualized; private readonly CircularBuffer _returns; private double _previousClose; private double _sumSquaredReturns; + private const int TradingDaysPerYear = 252; + private const double Epsilon = 1e-10; + private const bool DefaultIsAnnualized = true; /// The number of periods for volatility calculation. /// Whether to annualize the result (default true). /// Thrown when period is less than 2. - public Rv(int period, bool isAnnualized = true) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rv(int period, bool isAnnualized = DefaultIsAnnualized) { if (period < 2) { @@ -72,12 +77,14 @@ public class Rv : AbstractBase /// The data source object that publishes updates. /// The number of periods for volatility calculation. /// Whether to annualize the result (default true). - public Rv(object source, int period, bool isAnnualized = true) : this(period, isAnnualized) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rv(object source, int period, bool isAnnualized = DefaultIsAnnualized) : this(period, isAnnualized) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -86,6 +93,7 @@ public class Rv : AbstractBase _sumSquaredReturns = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -95,36 +103,46 @@ public class Rv : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateLogReturn(double currentPrice, double previousPrice) + { + return previousPrice > Epsilon ? Math.Log(currentPrice / previousPrice) : 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateVolatility(double sumSquaredReturns, int period, bool isAnnualized) + { + double variance = sumSquaredReturns / period; + double volatility = Math.Sqrt(variance); + return isAnnualized ? volatility * Math.Sqrt(TradingDaysPerYear) : volatility; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); double volatility = 0; - if (_previousClose != 0) + if (_previousClose > Epsilon) { // Calculate log return - double logReturn = Math.Log(Input.Value / _previousClose); + double logReturn = CalculateLogReturn(Input.Value, _previousClose); if (_returns.Count == Period) { // Maintain rolling sum by removing oldest squared return - _sumSquaredReturns -= Math.Pow(_returns[0], 2); + double oldReturn = _returns[0]; + _sumSquaredReturns -= oldReturn * oldReturn; } // Add new return and update sum _returns.Add(logReturn, Input.IsNew); - _sumSquaredReturns += Math.Pow(logReturn, 2); + _sumSquaredReturns += logReturn * logReturn; if (_returns.Count == Period) { // Calculate realized volatility - double variance = _sumSquaredReturns / Period; - volatility = Math.Sqrt(variance); - - if (IsAnnualized) - { - volatility *= Math.Sqrt(252); // Annualize using trading days - } + volatility = CalculateVolatility(_sumSquaredReturns, Period, IsAnnualized); } } diff --git a/lib/volatility/Rvi.cs b/lib/volatility/Rvi.cs index f6468816..555727fb 100644 --- a/lib/volatility/Rvi.cs +++ b/lib/volatility/Rvi.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -42,14 +42,18 @@ namespace QuanTAlib; /// Note: Similar concept to RSI but using volatility /// -public class Rvi : AbstractBase +[SkipLocalsInit] +public sealed class Rvi : AbstractBase { private readonly Stddev _upStdDev, _downStdDev; private readonly Sma _upSma, _downSma; private double _previousClose; + private const double ScalingFactor = 100.0; + private const double Epsilon = 1e-10; /// The number of periods for RVI calculation. /// Thrown when period is less than 2. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rvi(int period) { if (period < 2) @@ -57,30 +61,32 @@ public class Rvi : AbstractBase throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2."); } - int Period = period; WarmupPeriod = period; Name = $"RVI(period={period})"; - _upStdDev = new Stddev(Period); - _downStdDev = new Stddev(Period); - _upSma = new(Period); - _downSma = new(Period); + _upStdDev = new Stddev(period); + _downStdDev = new Stddev(period); + _upSma = new(period); + _downSma = new(period); Init(); } /// The data source object that publishes updates. /// The number of periods for RVI calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rvi(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _previousClose = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -90,6 +96,20 @@ public class Rvi : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double upMove, double downMove) CalculateMoves(double change) + { + return (Math.Max(change, 0), Math.Max(-change, 0)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateRvi(double upSma, double downSma) + { + double totalSma = upSma + downSma; + return totalSma > Epsilon ? ScalingFactor * upSma / totalSma : 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -98,18 +118,14 @@ public class Rvi : AbstractBase double change = close - _previousClose; // Separate into up and down moves - double upMove = Math.Max(change, 0); - double downMove = Math.Max(-change, 0); + var (upMove, downMove) = CalculateMoves(change); // Calculate standard deviations and apply smoothing _upSma.Calc(_upStdDev.Calc(new TValue(Input.Time, upMove, Input.IsNew))); _downSma.Calc(_downStdDev.Calc(new TValue(Input.Time, downMove, Input.IsNew))); // Calculate RVI ratio - double rvi; - rvi = (_upSma.Value + _downSma.Value != 0) - ? 100 * _upSma.Value / (_upSma.Value + _downSma.Value) - : 0; + double rvi = CalculateRvi(_upSma.Value, _downSma.Value); _previousClose = close; IsHot = _index >= WarmupPeriod; From 6b79f8158cbb3f37a8a1665f4af80fccf2f178ca Mon Sep 17 00:00:00 2001 From: Miha Date: Sun, 27 Oct 2024 21:59:46 -0700 Subject: [PATCH 06/17] Adx, Adxr, Apo, Dmi --- .vscode/settings.json | 14 +- Tests/Tests.csproj | 9 +- Tests/test_eventing.cs | 25 +++ Tests/test_updates_momentum.cs | 91 +++++++++++ Tests/test_updates_oscillators.cs | 64 ++++++++ Tests/test_updates_volatility.cs | 33 ++-- benchmark/benchmark.csproj | 28 ---- benchmark/growthBench.cs | 73 --------- docs/index.html | 10 +- lib/momentum/Adx.cs | 179 ++++++++++++++++++++++ lib/momentum/Adxr.cs | 90 +++++++++++ lib/momentum/Apo.cs | 92 +++++++++++ lib/momentum/Dmi.cs | 171 +++++++++++++++++++++ lib/momentum/_list.md | 6 +- lib/volatility/Jvolty.cs | 2 +- quantower/Momentum/_Momentum.csproj | 33 ++++ quantower/Oscillators/_Oscillators.csproj | 33 ++++ quantower/Volume/_Volume.csproj | 33 ++++ 18 files changed, 859 insertions(+), 127 deletions(-) create mode 100644 Tests/test_updates_momentum.cs create mode 100644 Tests/test_updates_oscillators.cs delete mode 100644 benchmark/benchmark.csproj delete mode 100644 benchmark/growthBench.cs create mode 100644 lib/momentum/Adx.cs create mode 100644 lib/momentum/Adxr.cs create mode 100644 lib/momentum/Apo.cs create mode 100644 lib/momentum/Dmi.cs create mode 100644 quantower/Momentum/_Momentum.csproj create mode 100644 quantower/Oscillators/_Oscillators.csproj create mode 100644 quantower/Volume/_Volume.csproj diff --git a/.vscode/settings.json b/.vscode/settings.json index 335e6e7c..e27b36ee 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,11 +7,17 @@ } ], "sarif-viewer.connectToGithubCodeScanning": "on", - "dotnet.dotnetPath": "/opt/homebrew/bin/", "omnisharp.useModernNet": true, - "omnisharp.sdkPath": "/opt/homebrew/bin/dotnet", "sonarlint.connectedMode.project": { "connectionId": "mihakralj", "projectKey": "mihakralj_QuanTAlib" - } -} \ No newline at end of file + }, + "dotnet.backgroundAnalysis.analyzerDiagnosticsScope": "fullSolution", + "dotnet.completion.showCompletionItemsFromUnimportedNamespaces": true, + "dotnetAcquisitionExtension.enableTelemetry": false, + "dotnet-test-explorer.testProjectPath": "Tests", + "dotnet-test-explorer.autoWatch": true, + "dotnet-test-explorer.showCodeLens": true, + "dotnet-test-explorer.testArguments": "/p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:CoverletOutput=./TestResults/coverage.cobertura.xml" + +} diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 11c1e048..31f09837 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -2,15 +2,16 @@ QuanTAlib.Tests QuanTAlib.Tests + false - - runtime; build; native; contentfiles; analyzers; buildtransitive - all + + runtime; build; native; contentfiles; analyzers; buildtransitive + all - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Tests/test_eventing.cs b/Tests/test_eventing.cs index 57f519ca..5b0d6db4 100644 --- a/Tests/test_eventing.cs +++ b/Tests/test_eventing.cs @@ -51,12 +51,37 @@ public class EventingTests ("Tema", new Tema(p), new Tema(input, p)), ("Kama", new Kama(2, 30, 6), new Kama(input, 2, 30, 6)), ("Zlema", new Zlema(p), new Zlema(input, p)), + // Added missing averages + ("Sinema", new Sinema(p), new Sinema(input, p)), + ("Smma", new Smma(p), new Smma(input, p)), + ("T3", new T3(p), new T3(input, p)), + ("Trima", new Trima(p), new Trima(input, p)), + ("Vidya", new Vidya(p), new Vidya(input, p)), + // momentum indicators + ("Apo", new Apo(12, 26), new Apo(input, 12, 26)), // oscillators ("Rsi", new Rsi(p), new Rsi(input, p)), ("Rsx", new Rsx(p), new Rsx(input, p)), ("Cmo", new Cmo(p), new Cmo(input, p)), + // statistics + ("Curvature", new Curvature(p), new Curvature(input, p)), + ("Entropy", new Entropy(p), new Entropy(input, p)), + ("Kurtosis", new Kurtosis(p), new Kurtosis(input, p)), + ("Max", new Max(p), new Max(input, p)), + ("Median", new Median(p), new Median(input, p)), + ("Min", new Min(p), new Min(input, p)), + ("Mode", new Mode(p), new Mode(input, p)), + ("Percentile", new Percentile(p, 0.5), new Percentile(input, p, 0.5)), + ("Skew", new Skew(p), new Skew(input, p)), + ("Slope", new Slope(p), new Slope(input, p)), + ("Stddev", new Stddev(p), new Stddev(input, p)), + ("Variance", new Variance(p), new Variance(input, p)), + ("Zscore", new Zscore(p), new Zscore(input, p)), // volatility + ("Hv", new Hv(p), new Hv(input, p)), + ("Jvolty", new Jvolty(p), new Jvolty(input, p)), ("Rv", new Rv(p), new Rv(input, p)), + ("Rvi", new Rvi(p), new Rvi(input, p)), // error classes ("Mae", new Mae(p), new Mae(input, p)), ("Mapd", new Mapd(p), new Mapd(input, p)), diff --git a/Tests/test_updates_momentum.cs b/Tests/test_updates_momentum.cs new file mode 100644 index 00000000..a94d2386 --- /dev/null +++ b/Tests/test_updates_momentum.cs @@ -0,0 +1,91 @@ +using Xunit; +using System.Security.Cryptography; + +namespace QuanTAlib.Tests; + +public class MomentumUpdateTests +{ + private readonly RandomNumberGenerator rng = RandomNumberGenerator.Create(); + private const int RandomUpdates = 100; + private const double ReferenceValue = 100.0; + private const int precision = 8; + + private double GetRandomDouble() + { + byte[] bytes = new byte[8]; + rng.GetBytes(bytes); + return (double)BitConverter.ToUInt64(bytes, 0) / ulong.MaxValue * 200 - 100; // Range: -100 to 100 + } + + private TBar GetRandomBar(bool IsNew) + { + double open = GetRandomDouble(); + double high = open + Math.Abs(GetRandomDouble()); + double low = open - Math.Abs(GetRandomDouble()); + double close = low + (high - low) * GetRandomDouble(); + return new TBar(DateTime.Now, open, high, low, close, 1000, IsNew); + } + + [Fact] + public void Adx_Update() + { + var indicator = new Adx(period: 14); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Adxr_Update() + { + var indicator = new Adxr(period: 14); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Apo_Update() + { + var indicator = new Apo(fastPeriod: 12, slowPeriod: 26); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Dmi_Update() + { + var indicator = new Dmi(period: 14); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } +} diff --git a/Tests/test_updates_oscillators.cs b/Tests/test_updates_oscillators.cs new file mode 100644 index 00000000..297f0ebd --- /dev/null +++ b/Tests/test_updates_oscillators.cs @@ -0,0 +1,64 @@ +using Xunit; +using System.Security.Cryptography; + +namespace QuanTAlib.Tests; + +public class OscillatorsUpdateTests +{ + private readonly RandomNumberGenerator rng = RandomNumberGenerator.Create(); + private const int RandomUpdates = 100; + private const double ReferenceValue = 100.0; + private const int precision = 8; + + private double GetRandomDouble() + { + byte[] bytes = new byte[8]; + rng.GetBytes(bytes); + return (double)BitConverter.ToUInt64(bytes, 0) / ulong.MaxValue * 200 - 100; // Range: -100 to 100 + } + + [Fact] + public void Rsi_Update() + { + var indicator = new Rsi(period: 14); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Rsx_Update() + { + var indicator = new Rsx(period: 14); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Cmo_Update() + { + var indicator = new Cmo(period: 14); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } +} diff --git a/Tests/test_updates_volatility.cs b/Tests/test_updates_volatility.cs index 5a041c26..f8877a8e 100644 --- a/Tests/test_updates_volatility.cs +++ b/Tests/test_updates_volatility.cs @@ -46,13 +46,28 @@ public class VolatilityUpdateTests public void Historical_Update() { var indicator = new Hv(period: 14); - double initialValue = indicator.Calc(new TBar(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true)); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); for (int i = 0; i < RandomUpdates; i++) { - indicator.Calc(GetRandomBar(false)); + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); } - double finalValue = indicator.Calc(new TBar(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: false)); + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Jvolty_Update() + { + var indicator = new Jvolty(period: 14); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); Assert.Equal(initialValue, finalValue, precision); } @@ -61,13 +76,13 @@ public class VolatilityUpdateTests public void Realized_Update() { var indicator = new Rv(period: 14); - double initialValue = indicator.Calc(new TBar(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true)); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); for (int i = 0; i < RandomUpdates; i++) { - indicator.Calc(GetRandomBar(false)); + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); } - double finalValue = indicator.Calc(new TBar(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: false)); + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); Assert.Equal(initialValue, finalValue, precision); } @@ -76,13 +91,13 @@ public class VolatilityUpdateTests public void Rvi_Update() { var indicator = new Rvi(period: 14); - double initialValue = indicator.Calc(new TBar(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true)); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); for (int i = 0; i < RandomUpdates; i++) { - indicator.Calc(GetRandomBar(false)); + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); } - double finalValue = indicator.Calc(new TBar(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: false)); + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); Assert.Equal(initialValue, finalValue, precision); } diff --git a/benchmark/benchmark.csproj b/benchmark/benchmark.csproj deleted file mode 100644 index 0940953e..00000000 --- a/benchmark/benchmark.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - Exe - net8.0 - false - enable - latest - false - - - - - - - - - - - - true - AnyCPU - pdbonly - true - true - - - \ No newline at end of file diff --git a/benchmark/growthBench.cs b/benchmark/growthBench.cs deleted file mode 100644 index 6dd123e4..00000000 --- a/benchmark/growthBench.cs +++ /dev/null @@ -1,73 +0,0 @@ -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Running; - -namespace QuanTAlib; - -public class Program -{ - public static void Main(string[] args) - { - var config = DefaultConfig.Instance - .WithOption(ConfigOptions.DisableOptimizationsValidator, true); - BenchmarkRunner.Run(config); - } -} - -[MemoryDiagnoser] -[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 3, iterationCount: 5)] -public class EmaBenchmark -{ - private const int Period = 10; - private const int Length = 100_000; - private GbmFeed gbm = null!; - private TSeries inputs = null!; - - [GlobalSetup] - public void Setup() - { - gbm = new GbmFeed(); - inputs = new(); - - for (int i = 0; i < Length; i++) - { - TBar item = gbm.Generate(DateTime.Now); - inputs.Add(new TValue(item.Time, item.Close, true, true)); - } - } - - [Benchmark] - public void Ema_bench() - { - Ema ma1 = new(Period); - for (int i = 0; i < Length; i++) - { - TValue item = gbm.Generate(DateTime.Now).Close; - ma1.Calc(item); - } - - } - - [Benchmark] - public void Alma_bench() - { - Alma ma1 = new(Period); - for (int i = 0; i < Length; i++) - { - TValue item = gbm.Generate(DateTime.Now).Close; - ma1.Calc(item); - } - } - - [Benchmark] - public void Dema_bench() - { - Dema ma1 = new(Period); - for (int i = 0; i < Length; i++) - { - TValue item = gbm.Generate(DateTime.Now).Close; - ma1.Calc(item); - } - } -} \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index c98425ed..eb060953 100644 --- a/docs/index.html +++ b/docs/index.html @@ -63,11 +63,11 @@ - - - - - + + + + + diff --git a/lib/momentum/Adx.cs b/lib/momentum/Adx.cs new file mode 100644 index 00000000..d5d061d9 --- /dev/null +++ b/lib/momentum/Adx.cs @@ -0,0 +1,179 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// ADX: Average Directional Movement Index +/// A technical analysis indicator used to measure the strength of a trend, +/// regardless of its direction. ADX combines the Positive and Negative +/// Directional Movement Indicators to determine trend strength. +/// +/// +/// The ADX calculation process: +/// 1. Calculate True Range (TR) +/// 2. Calculate +DM (Positive Directional Movement) +/// 3. Calculate -DM (Negative Directional Movement) +/// 4. Smooth TR, +DM, and -DM using Wilder's smoothing +/// 5. Calculate +DI and -DI +/// 6. Calculate DX (Directional Index) +/// 7. Smooth DX to get ADX +/// +/// Key characteristics: +/// - Oscillates between 0 and 100 +/// - Values above 25 indicate strong trend +/// - Values below 20 indicate weak or no trend +/// - Can be used with +DI and -DI for trade signals +/// - Does not indicate trend direction, only strength +/// +/// Formula: +/// TR = max(high-low, abs(high-prevClose), abs(low-prevClose)) +/// +DM = if(high-prevHigh > prevLow-low) then max(high-prevHigh, 0) else 0 +/// -DM = if(prevLow-low > high-prevHigh) then max(prevLow-low, 0) else 0 +/// +DI = 100 * smoothed(+DM) / smoothed(TR) +/// -DI = 100 * smoothed(-DM) / smoothed(TR) +/// DX = 100 * abs(+DI - -DI) / (+DI + -DI) +/// ADX = smoothed(DX) +/// +/// Sources: +/// J. Welles Wilder Jr. - "New Concepts in Technical Trading Systems" (1978) +/// https://www.investopedia.com/terms/a/adx.asp +/// +/// Note: Default period of 14 was recommended by Wilder +/// + +[SkipLocalsInit] +public sealed class Adx : AbstractBarBase +{ + private readonly Rma _smoothedTr; + private readonly Rma _smoothedPlusDm; + private readonly Rma _smoothedMinusDm; + private readonly Rma _smoothedDx; + private double _prevHigh, _prevLow, _prevClose; + private double _p_prevHigh, _p_prevLow, _p_prevClose; + private const double ScalingFactor = 100.0; + private const int DefaultPeriod = 14; + + /// The number of periods used in the ADX calculation (default 14). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Adx(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + _smoothedTr = new(period, useSma: true); + _smoothedPlusDm = new(period, useSma: true); + _smoothedMinusDm = new(period, useSma: true); + _smoothedDx = new(period, useSma: true); + _index = 0; + WarmupPeriod = period * 2; // Need extra period for DX smoothing + Name = $"ADX({period})"; + } + + /// The data source object that publishes updates. + /// The number of periods used in the ADX calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Adx(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _index++; + _p_prevHigh = _prevHigh; + _p_prevLow = _prevLow; + _p_prevClose = _prevClose; + } + else + { + _prevHigh = _p_prevHigh; + _prevLow = _p_prevLow; + _prevClose = _p_prevClose; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateTrueRange(double high, double low, double prevClose) + { + double hl = high - low; + double hpc = Math.Abs(high - prevClose); + double lpc = Math.Abs(low - prevClose); + return Math.Max(hl, Math.Max(hpc, lpc)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double plusDm, double minusDm) CalculateDirectionalMovement( + double high, double low, double prevHigh, double prevLow) + { + double upMove = high - prevHigh; + double downMove = prevLow - low; + + double plusDm = 0.0; + double minusDm = 0.0; + + if (upMove > downMove && upMove > 0) + plusDm = upMove; + else if (downMove > upMove && downMove > 0) + minusDm = downMove; + + return (plusDm, minusDm); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateDx(double plusDi, double minusDi) + { + double sum = plusDi + minusDi; + if (sum > 0) + return ScalingFactor * Math.Abs(plusDi - minusDi) / sum; + return 0.0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + if (_index == 1) + { + _prevHigh = Input.High; + _prevLow = Input.Low; + _prevClose = Input.Close; + return 0.0; + } + + // Calculate True Range and Directional Movement + double tr = CalculateTrueRange(Input.High, Input.Low, _prevClose); + var (plusDm, minusDm) = CalculateDirectionalMovement( + Input.High, Input.Low, _prevHigh, _prevLow); + + // Update previous values + _prevHigh = Input.High; + _prevLow = Input.Low; + _prevClose = Input.Close; + + // Smooth the indicators using Wilder's method + _smoothedTr.Calc(tr, Input.IsNew); + _smoothedPlusDm.Calc(plusDm, Input.IsNew); + _smoothedMinusDm.Calc(minusDm, Input.IsNew); + + // Calculate +DI and -DI + double smoothedTr = _smoothedTr.Value; + if (smoothedTr > 0) + { + double plusDi = ScalingFactor * _smoothedPlusDm.Value / smoothedTr; + double minusDi = ScalingFactor * _smoothedMinusDm.Value / smoothedTr; + + // Calculate DX + double dx = CalculateDx(plusDi, minusDi); + + // Smooth DX to get ADX + _smoothedDx.Calc(dx, Input.IsNew); + return _smoothedDx.Value; + } + + return 0.0; + } +} diff --git a/lib/momentum/Adxr.cs b/lib/momentum/Adxr.cs new file mode 100644 index 00000000..e2457f41 --- /dev/null +++ b/lib/momentum/Adxr.cs @@ -0,0 +1,90 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// ADXR: Average Directional Movement Index Rating +/// A momentum indicator that measures trend strength by comparing the current ADX +/// value with a historical ADX value. ADXR helps identify potential trend +/// reversals earlier than standard ADX. +/// +/// +/// The ADXR calculation process: +/// 1. Calculate current period ADX +/// 2. Calculate historical period ADX (shifted back by period) +/// 3. Average the current and historical ADX values +/// +/// Key characteristics: +/// - Oscillates between 0 and 100 +/// - Values above 25 indicate strong trend +/// - Values below 20 indicate weak or no trend +/// - Faster at identifying trend changes than ADX +/// - Does not indicate trend direction, only strength +/// +/// Formula: +/// ADXR = (Current ADX + Historical ADX) / 2 +/// where: +/// Historical ADX = ADX value from 'period' bars ago +/// +/// Sources: +/// J. Welles Wilder Jr. - "New Concepts in Technical Trading Systems" (1978) +/// https://www.investopedia.com/terms/a/adxr.asp +/// +/// Note: Default period of 14 was recommended by Wilder +/// + +[SkipLocalsInit] +public sealed class Adxr : AbstractBarBase +{ + private readonly Adx _currentAdx; + private readonly CircularBuffer _historicalAdx; + private const int DefaultPeriod = 14; + + /// The number of periods used in the ADXR calculation (default 14). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Adxr(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + _currentAdx = new(period); + _historicalAdx = new(period); + _index = 0; + WarmupPeriod = period * 3; // Need extra periods for historical ADX + Name = $"ADXR({period})"; + } + + /// The data source object that publishes updates. + /// The number of periods used in the ADXR calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Adxr(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + _index++; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + // Calculate current ADX + double currentAdx = _currentAdx.Value; + _currentAdx.Calc(Input); + + // Store ADX value in historical buffer + _historicalAdx.Add(currentAdx, Input.IsNew); + + // Calculate ADXR once we have enough historical data + if (_index > _historicalAdx.Capacity) + return (currentAdx + _historicalAdx.Oldest()) / 2.0; + + return currentAdx; + } +} diff --git a/lib/momentum/Apo.cs b/lib/momentum/Apo.cs new file mode 100644 index 00000000..686d9b5c --- /dev/null +++ b/lib/momentum/Apo.cs @@ -0,0 +1,92 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// APO: Absolute Price Oscillator +/// A momentum indicator that measures the absolute difference between two moving +/// averages of different periods. APO helps identify trend direction and potential +/// reversals by showing the momentum of price movement. +/// +/// +/// The APO calculation process: +/// 1. Calculate fast period moving average +/// 2. Calculate slow period moving average +/// 3. Calculate absolute difference between the two averages +/// +/// Key characteristics: +/// - Oscillates above and below zero +/// - Positive values indicate upward price momentum +/// - Negative values indicate downward price momentum +/// - Zero line crossovers signal potential trend changes +/// - Similar to MACD but uses simple moving averages +/// +/// Formula: +/// APO = Fast MA - Slow MA +/// where: +/// Fast MA = Moving average of shorter period +/// Slow MA = Moving average of longer period +/// +/// Sources: +/// https://www.investopedia.com/terms/p/ppo.asp +/// https://school.stockcharts.com/doku.php?id=technical_indicators:price_oscillators_ppo +/// +/// Note: Default periods are 12 and 26, similar to MACD +/// + +[SkipLocalsInit] +public sealed class Apo : AbstractBase +{ + private readonly Sma _fastMa; + private readonly Sma _slowMa; + private const int DefaultFastPeriod = 12; + private const int DefaultSlowPeriod = 26; + + /// The number of periods for the fast moving average (default 12). + /// The number of periods for the slow moving average (default 26). + /// Thrown when either period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Apo(int fastPeriod = DefaultFastPeriod, int slowPeriod = DefaultSlowPeriod) + { + if (fastPeriod < 1) + throw new ArgumentOutOfRangeException(nameof(fastPeriod)); + if (slowPeriod < 1) + throw new ArgumentOutOfRangeException(nameof(slowPeriod)); + if (fastPeriod >= slowPeriod) + throw new ArgumentException("Fast period must be less than slow period"); + + _fastMa = new(fastPeriod); + _slowMa = new(slowPeriod); + WarmupPeriod = slowPeriod; + Name = $"APO({fastPeriod},{slowPeriod})"; + } + + /// The data source object that publishes updates. + /// The number of periods for the fast moving average. + /// The number of periods for the slow moving average. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Apo(object source, int fastPeriod, int slowPeriod) : this(fastPeriod, slowPeriod) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + _index++; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + // Calculate both moving averages + double fastMa = _fastMa.Calc(Input.Value, Input.IsNew); + double slowMa = _slowMa.Calc(Input.Value, Input.IsNew); + + // Calculate absolute difference + return fastMa - slowMa; + } +} diff --git a/lib/momentum/Dmi.cs b/lib/momentum/Dmi.cs new file mode 100644 index 00000000..f0c7c49f --- /dev/null +++ b/lib/momentum/Dmi.cs @@ -0,0 +1,171 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// DMI: Directional Movement Index +/// A technical indicator that identifies the directional movement of price by +/// comparing successive highs and lows. DMI consists of two lines: +DI and -DI, +/// which help determine trend direction and strength. +/// +/// +/// The DMI calculation process: +/// 1. Calculate True Range (TR) +/// 2. Calculate +DM (Positive Directional Movement) +/// 3. Calculate -DM (Negative Directional Movement) +/// 4. Smooth TR, +DM, and -DM using Wilder's smoothing +/// 5. Calculate +DI and -DI as percentages +/// +/// Key characteristics: +/// - Both +DI and -DI oscillate between 0 and 100 +/// - When +DI > -DI, uptrend is indicated +/// - When -DI > +DI, downtrend is indicated +/// - Crossovers of +DI and -DI signal potential trend changes +/// - Used in conjunction with ADX for trend trading +/// +/// Formula: +/// TR = max(high-low, abs(high-prevClose), abs(low-prevClose)) +/// +DM = if(high-prevHigh > prevLow-low) then max(high-prevHigh, 0) else 0 +/// -DM = if(prevLow-low > high-prevHigh) then max(prevLow-low, 0) else 0 +/// +DI = 100 * smoothed(+DM) / smoothed(TR) +/// -DI = 100 * smoothed(-DM) / smoothed(TR) +/// +/// Sources: +/// J. Welles Wilder Jr. - "New Concepts in Technical Trading Systems" (1978) +/// https://www.investopedia.com/terms/d/dmi.asp +/// +/// Note: Default period of 14 was recommended by Wilder +/// + +[SkipLocalsInit] +public sealed class Dmi : AbstractBarBase +{ + private readonly Rma _smoothedTr; + private readonly Rma _smoothedPlusDm; + private readonly Rma _smoothedMinusDm; + private double _prevHigh, _prevLow, _prevClose; + private double _p_prevHigh, _p_prevLow, _p_prevClose; + private double _plusDi, _minusDi; + private const double ScalingFactor = 100.0; + private const int DefaultPeriod = 14; + + /// + /// Gets the most recent +DI value + /// + public double PlusDI => _plusDi; + + /// + /// Gets the most recent -DI value + /// + public double MinusDI => _minusDi; + + /// The number of periods used in the DMI calculation (default 14). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Dmi(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + _smoothedTr = new(period, useSma: true); + _smoothedPlusDm = new(period, useSma: true); + _smoothedMinusDm = new(period, useSma: true); + _index = 0; + WarmupPeriod = period + 1; + Name = $"DMI({period})"; + } + + /// The data source object that publishes updates. + /// The number of periods used in the DMI calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Dmi(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _index++; + _p_prevHigh = _prevHigh; + _p_prevLow = _prevLow; + _p_prevClose = _prevClose; + } + else + { + _prevHigh = _p_prevHigh; + _prevLow = _p_prevLow; + _prevClose = _p_prevClose; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateTrueRange(double high, double low, double prevClose) + { + double hl = high - low; + double hpc = Math.Abs(high - prevClose); + double lpc = Math.Abs(low - prevClose); + return Math.Max(hl, Math.Max(hpc, lpc)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double plusDm, double minusDm) CalculateDirectionalMovement( + double high, double low, double prevHigh, double prevLow) + { + double upMove = high - prevHigh; + double downMove = prevLow - low; + + double plusDm = 0.0; + double minusDm = 0.0; + + if (upMove > downMove && upMove > 0) + plusDm = upMove; + else if (downMove > upMove && downMove > 0) + minusDm = downMove; + + return (plusDm, minusDm); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + if (_index == 1) + { + _prevHigh = Input.High; + _prevLow = Input.Low; + _prevClose = Input.Close; + return 0.0; + } + + // Calculate True Range and Directional Movement + double tr = CalculateTrueRange(Input.High, Input.Low, _prevClose); + var (plusDm, minusDm) = CalculateDirectionalMovement( + Input.High, Input.Low, _prevHigh, _prevLow); + + // Update previous values + _prevHigh = Input.High; + _prevLow = Input.Low; + _prevClose = Input.Close; + + // Smooth the indicators using Wilder's method + _smoothedTr.Calc(tr, Input.IsNew); + _smoothedPlusDm.Calc(plusDm, Input.IsNew); + _smoothedMinusDm.Calc(minusDm, Input.IsNew); + + // Calculate +DI and -DI + double smoothedTr = _smoothedTr.Value; + if (smoothedTr > 0) + { + _plusDi = ScalingFactor * _smoothedPlusDm.Value / smoothedTr; + _minusDi = ScalingFactor * _smoothedMinusDm.Value / smoothedTr; + return _plusDi - _minusDi; // Return the difference as main value + } + + _plusDi = 0.0; + _minusDi = 0.0; + return 0.0; + } +} diff --git a/lib/momentum/_list.md b/lib/momentum/_list.md index 873798b0..398da442 100644 --- a/lib/momentum/_list.md +++ b/lib/momentum/_list.md @@ -1,6 +1,6 @@ -ADX - Average Directional Movement Index -ADXR - Average Directional Movement Index -APO - Absolute Price Oscillator +✔️ ADX - Average Directional Movement Index +✔️ ADXR - Average Directional Movement Index Rating +✔️ APO - Absolute Price Oscillator DMI - Directional Movement Index DMX - Jurik Directional Movement Index DPO - Detrended Price Oscillator diff --git a/lib/volatility/Jvolty.cs b/lib/volatility/Jvolty.cs index 424bc8cb..ffac4d58 100644 --- a/lib/volatility/Jvolty.cs +++ b/lib/volatility/Jvolty.cs @@ -96,7 +96,7 @@ public sealed class Jvolty : AbstractBase public Jvolty(object source, int period, int phase = DefaultPhase) : this(period, phase) { var pubEvent = source.GetType().GetEvent("Pub"); - pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/quantower/Momentum/_Momentum.csproj b/quantower/Momentum/_Momentum.csproj new file mode 100644 index 00000000..00cb1a71 --- /dev/null +++ b/quantower/Momentum/_Momentum.csproj @@ -0,0 +1,33 @@ + + + Momentum + Indicator + 0.0.0.0 + bin\$(Configuration)\ + true + true + true + false + + + + + + + + + + + ..\..\.github\TradingPlatform.BusinessLayer.dll + + + TradingPlatform.BusinessLayer.xml + + + + + + + + diff --git a/quantower/Oscillators/_Oscillators.csproj b/quantower/Oscillators/_Oscillators.csproj new file mode 100644 index 00000000..ed3a39fe --- /dev/null +++ b/quantower/Oscillators/_Oscillators.csproj @@ -0,0 +1,33 @@ + + + Oscillators + Indicator + 0.0.0.0 + bin\$(Configuration)\ + true + true + true + false + + + + + + + + + + + ..\..\.github\TradingPlatform.BusinessLayer.dll + + + TradingPlatform.BusinessLayer.xml + + + + + + + + diff --git a/quantower/Volume/_Volume.csproj b/quantower/Volume/_Volume.csproj new file mode 100644 index 00000000..13266f2a --- /dev/null +++ b/quantower/Volume/_Volume.csproj @@ -0,0 +1,33 @@ + + + Volume + Indicator + 0.0.0.0 + bin\$(Configuration)\ + true + true + true + false + + + + + + + + + + + ..\..\.github\TradingPlatform.BusinessLayer.dll + + + TradingPlatform.BusinessLayer.xml + + + + + + + + From 45c6f08e1eec880cf91f587c3042cef131ee6e25 Mon Sep 17 00:00:00 2001 From: Miha Date: Mon, 28 Oct 2024 15:29:29 -0700 Subject: [PATCH 07/17] Adl, Adosc, Aobv, Cmf --- Tests/test_core.cs | 251 +++++++++++++++++++++++++++++++++++ Tests/test_eventing.cs | 69 +++++++--- Tests/test_updates_volume.cs | 159 ++++++++++++++++++++++ lib/momentum/_list.md | 2 + lib/oscillators/_list.md | 2 + lib/statistics/_list.md | 2 + lib/volatility/_list.md | 4 +- lib/volume/Adl.cs | 113 ++++++++++++++++ lib/volume/Adosc.cs | 138 +++++++++++++++++++ lib/volume/Aobv.cs | 133 +++++++++++++++++++ lib/volume/Cmf.cs | 129 ++++++++++++++++++ lib/volume/Eom.cs | 132 ++++++++++++++++++ lib/volume/Kvo.cs | 141 ++++++++++++++++++++ lib/volume/_list.md | 14 +- 14 files changed, 1266 insertions(+), 23 deletions(-) create mode 100644 Tests/test_core.cs create mode 100644 Tests/test_updates_volume.cs create mode 100644 lib/volume/Adl.cs create mode 100644 lib/volume/Adosc.cs create mode 100644 lib/volume/Aobv.cs create mode 100644 lib/volume/Cmf.cs create mode 100644 lib/volume/Eom.cs create mode 100644 lib/volume/Kvo.cs diff --git a/Tests/test_core.cs b/Tests/test_core.cs new file mode 100644 index 00000000..49d67d4b --- /dev/null +++ b/Tests/test_core.cs @@ -0,0 +1,251 @@ +using Xunit; + +namespace QuanTAlib.Tests; + +public class CoreTests +{ + #region CircularBuffer Tests + + [Fact] + public void CircularBuffer_BasicOperations() + { + var buffer = new CircularBuffer(5); + + // Test initial state + Assert.Equal(5, buffer.Capacity); + Assert.Equal(0, buffer.Count); + + // Test adding items + buffer.Add(1.0); + buffer.Add(2.0); + Assert.Equal(2, buffer.Count); + Assert.Equal(1.0, buffer[0]); + Assert.Equal(2.0, buffer[^1]); + + // Test overflow behavior + buffer.Add(3.0); + buffer.Add(4.0); + buffer.Add(5.0); + buffer.Add(6.0); // Should remove oldest item (1.0) + Assert.Equal(5, buffer.Count); + Assert.Equal(2.0, buffer[0]); + Assert.Equal(6.0, buffer[^1]); + } + + [Fact] + public void CircularBuffer_UpdateBehavior() + { + var buffer = new CircularBuffer(3); + + // Add new values + buffer.Add(1.0, isNew: true); + buffer.Add(2.0, isNew: true); + Assert.Equal(2, buffer.Count); + + // Update last value + buffer.Add(2.5, isNew: false); + Assert.Equal(2, buffer.Count); + Assert.Equal(2.5, buffer[^1]); + } + + [Fact] + public void CircularBuffer_MinMaxSumAverage() + { + var buffer = new CircularBuffer(5); + + buffer.Add(1.0); + buffer.Add(2.0); + buffer.Add(3.0); + buffer.Add(4.0); + buffer.Add(5.0); + + Assert.Equal(1.0, buffer.Min()); + Assert.Equal(5.0, buffer.Max()); + Assert.Equal(15.0, buffer.Sum()); + Assert.Equal(3.0, buffer.Average()); + } + + [Fact] + public void CircularBuffer_Enumeration() + { + var buffer = new CircularBuffer(3); + + buffer.Add(1.0); + buffer.Add(2.0); + buffer.Add(3.0); + + var list = buffer.ToList(); + Assert.Equal(3, list.Count); + Assert.Equal(1.0, list[0]); + Assert.Equal(3.0, list[2]); + } + + #endregion + + #region TBar Tests + + [Fact] + public void TBar_Construction() + { + // Default constructor + var bar1 = new TBar(); + Assert.Equal(0, bar1.Open); + Assert.True(bar1.IsNew); + + // Value constructor + var bar2 = new TBar(10.0); + Assert.Equal(10.0, bar2.Open); + Assert.Equal(10.0, bar2.High); + Assert.Equal(10.0, bar2.Low); + Assert.Equal(10.0, bar2.Close); + + // Full constructor + var time = DateTime.Now; + var bar3 = new TBar(time, 10.0, 12.0, 9.0, 11.0, 1000.0, false); + Assert.Equal(time, bar3.Time); + Assert.Equal(10.0, bar3.Open); + Assert.Equal(12.0, bar3.High); + Assert.Equal(9.0, bar3.Low); + Assert.Equal(11.0, bar3.Close); + Assert.Equal(1000.0, bar3.Volume); + Assert.False(bar3.IsNew); + } + + [Fact] + public void TBar_DerivedValues() + { + var bar = new TBar(DateTime.Now, 10.0, 20.0, 5.0, 15.0, 1000.0); + + Assert.Equal(12.5, bar.HL2); // (20 + 5) / 2 + Assert.Equal(12.5, bar.OC2); // (10 + 15) / 2 + Assert.Equal(11.67, bar.OHL3, 2); // (10 + 20 + 5) / 3 + Assert.Equal(13.33, bar.HLC3, 2); // (20 + 5 + 15) / 3 + Assert.Equal(12.5, bar.OHLC4); // (10 + 20 + 5 + 15) / 4 + Assert.Equal(13.75, bar.HLCC4); // (20 + 5 + 15 + 15) / 4 + } + + [Fact] + public void TBarSeries_Operations() + { + var series = new TBarSeries(); + var time = DateTime.Now; + var bar1 = new TBar(time, 10.0, 12.0, 9.0, 11.0, 1000.0); + var bar2 = new TBar(time.AddMinutes(1), 11.0, 13.0, 10.0, 12.0, 1100.0); + + // Test adding bars + series.Add(bar1); + series.Add(bar2); + Assert.Equal(2, series.Count); + + // Test updating last bar + var bar2Update = new TBar(bar2.Time, 11.0, 13.5, 9.5, 12.5, 1200.0, false); + series.Add(bar2Update); + Assert.Equal(2, series.Count); + Assert.Equal(12.5, series.Last.Close); + + // Test derived series + Assert.Equal(11.0, series.Open.Last.Value); + Assert.Equal(13.5, series.High.Last.Value); + Assert.Equal(9.5, series.Low.Last.Value); + Assert.Equal(12.5, series.Close.Last.Value); + Assert.Equal(1200.0, series.Volume.Last.Value); + } + + #endregion + + #region TValue Tests + + [Fact] + public void TValue_Construction() + { + // Default constructor + var value1 = new TValue(); + Assert.Equal(0, value1.Value); + Assert.True(value1.IsNew); + Assert.True(value1.IsHot); + + // Value constructor + var value2 = new TValue(10.0); + Assert.Equal(10.0, value2.Value); + + // Full constructor + var time = DateTime.Now; + var value3 = new TValue(time, 10.0, false, false); + Assert.Equal(time, value3.Time); + Assert.Equal(10.0, value3.Value); + Assert.False(value3.IsNew); + Assert.False(value3.IsHot); + } + + [Fact] + public void TValue_Conversions() + { + var value = new TValue(10.0); + + // Test implicit conversions + double d = value; + Assert.Equal(10.0, d); + + DateTime time = value; + Assert.Equal(value.Time, time); + + // Test implicit conversion from double + TValue newValue = 20.0; + Assert.Equal(20.0, newValue.Value); + } + + [Fact] + public void TSeries_Operations() + { + var series = new TSeries(); + var time = DateTime.Now; + + // Test adding values + series.Add(time, 10.0); + series.Add(time.AddMinutes(1), 20.0); + Assert.Equal(2, series.Count); + + // Test updating last value + series.Add(new TValue(time.AddMinutes(1), 25.0, false)); + Assert.Equal(2, series.Count); + Assert.Equal(25.0, series.Last.Value); + + // Test adding range of values + var values = new[] { 30.0, 40.0, 50.0 }; + foreach (var value in values) + { + series.Add(time.AddMinutes(series.Count + 1), value); + } + Assert.Equal(5, series.Count); + + // Test conversions + var doubleList = (List)series; + Assert.Equal(5, doubleList.Count); + Assert.Equal(50.0, doubleList[^1]); + + var doubleArray = (double[])series; + Assert.Equal(5, doubleArray.Length); + Assert.Equal(50.0, doubleArray[^1]); + } + + [Fact] + public void TSeries_EventHandling() + { + var series = new TSeries(); + var receivedValues = new List(); + var time = DateTime.Now; + + series.Pub += (object sender, in ValueEventArgs args) => receivedValues.Add(args.Tick.Value); + + series.Add(time, 10.0); + series.Add(time.AddMinutes(1), 20.0); + series.Add(time.AddMinutes(2), 30.0); + + Assert.Equal(3, receivedValues.Count); + Assert.Equal(10.0, receivedValues[0]); + Assert.Equal(20.0, receivedValues[1]); + Assert.Equal(30.0, receivedValues[2]); + } + + #endregion +} diff --git a/Tests/test_eventing.cs b/Tests/test_eventing.cs index 5b0d6db4..3564894e 100644 --- a/Tests/test_eventing.cs +++ b/Tests/test_eventing.cs @@ -13,12 +13,13 @@ public class EventingTests // Create a cryptographically secure random number generator using var rng = RandomNumberGenerator.Create(); - // Create an input series to hold our random values + // Create input series to hold our random values var input = new TSeries(); + var barInput = new TBarSeries(); int p = 10; - // Create a list of indicator pairs (direct calculation and event-based) with names - var indicators = new List<(string Name, AbstractBase Direct, AbstractBase EventBased)> + // Create a list of value-based indicator pairs + var valueIndicators = new List<(string Name, AbstractBase Direct, AbstractBase EventBased)> { ("Afirma", new Afirma(p,p,Afirma.WindowType.BlackmanHarris), new Afirma(input, p,p,Afirma.WindowType.BlackmanHarris)), ("Alma", new Alma(p), new Alma(input, p)), @@ -51,19 +52,15 @@ public class EventingTests ("Tema", new Tema(p), new Tema(input, p)), ("Kama", new Kama(2, 30, 6), new Kama(input, 2, 30, 6)), ("Zlema", new Zlema(p), new Zlema(input, p)), - // Added missing averages ("Sinema", new Sinema(p), new Sinema(input, p)), ("Smma", new Smma(p), new Smma(input, p)), ("T3", new T3(p), new T3(input, p)), ("Trima", new Trima(p), new Trima(input, p)), ("Vidya", new Vidya(p), new Vidya(input, p)), - // momentum indicators ("Apo", new Apo(12, 26), new Apo(input, 12, 26)), - // oscillators ("Rsi", new Rsi(p), new Rsi(input, p)), ("Rsx", new Rsx(p), new Rsx(input, p)), ("Cmo", new Cmo(p), new Cmo(input, p)), - // statistics ("Curvature", new Curvature(p), new Curvature(input, p)), ("Entropy", new Entropy(p), new Entropy(input, p)), ("Kurtosis", new Kurtosis(p), new Kurtosis(input, p)), @@ -77,12 +74,12 @@ public class EventingTests ("Stddev", new Stddev(p), new Stddev(input, p)), ("Variance", new Variance(p), new Variance(input, p)), ("Zscore", new Zscore(p), new Zscore(input, p)), - // volatility + // Volatility indicators (value-based) ("Hv", new Hv(p), new Hv(input, p)), ("Jvolty", new Jvolty(p), new Jvolty(input, p)), ("Rv", new Rv(p), new Rv(input, p)), ("Rvi", new Rvi(p), new Rvi(input, p)), - // error classes + // Error classes ("Mae", new Mae(p), new Mae(input, p)), ("Mapd", new Mapd(p), new Mapd(input, p)), ("Mape", new Mape(p), new Mape(input, p)), @@ -101,26 +98,66 @@ public class EventingTests ("Huber", new Huber(p), new Huber(input, p)) }; - // Generate 200 random values and feed them to both direct and event-based indicators + // Create a list of bar-based indicator pairs + var barIndicators = new List<(string Name, AbstractBase Direct, AbstractBase EventBased)> + { + // Volume indicators + ("Adl", new Adl(), new Adl(barInput)), + ("Adosc", new Adosc(3, 10), new Adosc(barInput, 3, 10)), + ("Aobv", new Aobv(), new Aobv(barInput)), + ("Cmf", new Cmf(20), new Cmf(barInput, 20)), + ("Eom", new Eom(14), new Eom(barInput, 14)), + ("Kvo", new Kvo(34, 55), new Kvo(barInput, 34, 55)), + // Volatility indicators (bar-based) + ("Atr", new Atr(14), new Atr(barInput, 14)) + }; + + // Generate 200 random values and feed them to indicators for (int i = 0; i < 200; i++) { + // Generate random value for value-based indicators double randomValue = GetRandomDouble(rng) * 100; input.Add(randomValue); - // Calculate direct indicators - foreach (var (_, direct, _) in indicators) + // Calculate value-based indicators + foreach (var (_, direct, _) in valueIndicators) { direct.Calc(randomValue); } + + // Generate random bar for bar-based indicators + var bar = new TBar( + DateTime.Now, + randomValue, + randomValue + Math.Abs(GetRandomDouble(rng) * 10), + randomValue - Math.Abs(GetRandomDouble(rng) * 10), + randomValue + GetRandomDouble(rng) * 5, + Math.Abs(GetRandomDouble(rng) * 1000), + true + ); + barInput.Add(bar); + + // Calculate bar-based indicators + foreach (var (_, direct, _) in barIndicators) + { + direct.Calc(bar); + } } - // Compare the results of direct and event-based calculations - for (int i = 0; i < indicators.Count; i++) + // Compare the results for value-based indicators + foreach (var (name, direct, eventBased) in valueIndicators) { - var (name, direct, eventBased) = indicators[i]; bool areEqual = (double.IsNaN(direct.Value) && double.IsNaN(eventBased.Value)) || Math.Abs(direct.Value - eventBased.Value) < 1e-9; - Assert.True(areEqual, $"Indicator {name} failed: Expected {direct.Value}, Actual {eventBased.Value}"); + Assert.True(areEqual, $"Value indicator {name} failed: Expected {direct.Value}, Actual {eventBased.Value}"); + } + + // Compare the results for bar-based indicators + foreach (var (name, direct, eventBased) in barIndicators) + { + bool areEqual = (double.IsNaN(direct.Value) && double.IsNaN(eventBased.Value)) || + Math.Abs(direct.Value - eventBased.Value) < 1e-9; + Assert.True(areEqual, $"Bar indicator {name} failed: Expected {direct.Value}, Actual {eventBased.Value}"); } } diff --git a/Tests/test_updates_volume.cs b/Tests/test_updates_volume.cs new file mode 100644 index 00000000..472dd740 --- /dev/null +++ b/Tests/test_updates_volume.cs @@ -0,0 +1,159 @@ +using Xunit; +using System.Security.Cryptography; + +namespace QuanTAlib.Tests; + +public class VolumeUpdateTests +{ + private readonly RandomNumberGenerator rng = RandomNumberGenerator.Create(); + private const int RandomUpdates = 100; + private const int precision = 8; + + private double GetRandomDouble() + { + byte[] bytes = new byte[8]; + rng.GetBytes(bytes); + return (double)BitConverter.ToUInt64(bytes, 0) / ulong.MaxValue * 200 - 100; // Range: -100 to 100 + } + + private TBar GetRandomBar(bool IsNew) + { + double open = GetRandomDouble(); + double high = open + Math.Abs(GetRandomDouble()); + double low = open - Math.Abs(GetRandomDouble()); + double close = low + (high - low) * GetRandomDouble(); + double volume = Math.Abs(GetRandomDouble()) * 1000; // Random positive volume + return new TBar(DateTime.Now, open, high, low, close, volume, IsNew); + } + + [Fact] + public void Adl_Update() + { + var indicator = new Adl(); + TBar r = GetRandomBar(true); + + // First calculation with IsNew: true + double value1 = indicator.Calc(r); + + // Multiple recalculations with IsNew: false should not change the value + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + } + + // Final calculation with IsNew: false should match initial value + double value2 = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + Assert.Equal(value1, value2, precision); + + // New calculation with IsNew: true should update the value + double value3 = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: true)); + Assert.NotEqual(value1, value3, precision); + } + + [Fact] + public void Adosc_Update() + { + var indicator = new Adosc(shortPeriod: 3, longPeriod: 10); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Aobv_Update() + { + var indicator = new Aobv(); + TBar r = GetRandomBar(true); + + // First calculation with IsNew: true + double value1 = indicator.Calc(r); + + // Multiple recalculations with IsNew: false should not change the value + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + } + + // Final calculation with IsNew: false should match initial value + double value2 = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + Assert.Equal(value1, value2, precision); + + // New calculation with IsNew: true should update the value + double value3 = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: true)); + Assert.NotEqual(value1, value3, precision); + } + + [Fact] + public void Cmf_Update() + { + var indicator = new Cmf(period: 20); + TBar r = GetRandomBar(true); + + // Generate a sequence of bars for warmup + var warmupBars = new List(); + for (int i = 0; i < indicator.WarmupPeriod; i++) + { + var bar = GetRandomBar(IsNew: true); + warmupBars.Add(bar); + indicator.Calc(bar); + } + + // Calculate initial value after warmup + double initialValue = indicator.Calc(r); + + // Apply random updates + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + + // Reset and replay the same sequence + indicator.Init(); + foreach (var bar in warmupBars) + { + indicator.Calc(bar); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Eom_Update() + { + var indicator = new Eom(period: 14); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Kvo_Update() + { + var indicator = new Kvo(shortPeriod: 34, longPeriod: 55); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } +} diff --git a/lib/momentum/_list.md b/lib/momentum/_list.md index 398da442..89739c87 100644 --- a/lib/momentum/_list.md +++ b/lib/momentum/_list.md @@ -1,3 +1,5 @@ +# Momentum indicators + ✔️ ADX - Average Directional Movement Index ✔️ ADXR - Average Directional Movement Index Rating ✔️ APO - Absolute Price Oscillator diff --git a/lib/oscillators/_list.md b/lib/oscillators/_list.md index ecda4263..618c07b3 100644 --- a/lib/oscillators/_list.md +++ b/lib/oscillators/_list.md @@ -1,3 +1,5 @@ +# Oscillators indicators + AC - Acceleration Oscillator AO - Awesome Oscillator AROON - Aroon oscillator diff --git a/lib/statistics/_list.md b/lib/statistics/_list.md index b7f77ddc..84fa4b37 100644 --- a/lib/statistics/_list.md +++ b/lib/statistics/_list.md @@ -1,3 +1,5 @@ +# Statistics indicators + BETA - Beta coefficient CORR - Correlation Coefficient ✔️ CURVATURE - Rate of Change in Direction or Slope diff --git a/lib/volatility/_list.md b/lib/volatility/_list.md index 5f62b5cf..1ec6089e 100644 --- a/lib/volatility/_list.md +++ b/lib/volatility/_list.md @@ -1,3 +1,5 @@ +# Volatility indicators + ADR - Average Daily Range AP - Andrew's Pitchfork ✔️ ATR - Average True Range @@ -22,7 +24,7 @@ PSAR - Parabolic Stop and Reverse PV - Parkinson Volatility RSV - Rogers-Satchell Volatility ✔️ RV - Realized Volatility -RVI - Relative Volatility Index +✔️ RVI - Relative Volatility Index STARC - Starc Bands SV - Stochastic Volatility TR - True Range diff --git a/lib/volume/Adl.cs b/lib/volume/Adl.cs new file mode 100644 index 00000000..786233ab --- /dev/null +++ b/lib/volume/Adl.cs @@ -0,0 +1,113 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// ADL: Accumulation Distribution Line (Chaikin) +/// A volume-based indicator that measures the cumulative flow of money into and out +/// of a security. It assesses the relationship between price and volume to determine +/// buying/selling pressure. +/// +/// +/// The ADL calculation process: +/// 1. Calculates Money Flow Multiplier (MFM): +/// MFM = ((Close - Low) - (High - Close)) / (High - Low) +/// 2. Calculates Money Flow Volume (MFV): +/// MFV = MFM × Volume +/// 3. ADL is cumulative sum of MFV values +/// +/// Key characteristics: +/// - Volume-weighted measure +/// - Cumulative indicator +/// - No upper/lower bounds +/// - Trend confirmation tool +/// - Divergence indicator +/// +/// Formula: +/// MFM = ((Close - Low) - (High - Close)) / (High - Low) +/// MFV = MFM × Volume +/// ADL = Previous ADL + MFV +/// +/// Market Applications: +/// - Trend confirmation +/// - Volume analysis +/// - Price/volume divergence +/// - Support/resistance levels +/// - Market participation +/// +/// Sources: +/// Marc Chaikin - Original development +/// https://www.investopedia.com/terms/a/accumulationdistribution.asp +/// +/// Note: Focuses on the relationship between price and volume +/// + +[SkipLocalsInit] +public sealed class Adl : AbstractBase +{ + private double _cumulativeAdl; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Adl() + { + WarmupPeriod = 1; + Name = "ADL"; + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Adl(object source) : this() + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _cumulativeAdl = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateMoneyFlowMultiplier(double close, double high, double low) + { + double range = high - low; + if (range > 0) + { + return ((close - low) - (high - close)) / range; + } + return 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Calculate Money Flow Multiplier + double mfm = CalculateMoneyFlowMultiplier(BarInput.Close, BarInput.High, BarInput.Low); + + // Calculate Money Flow Volume + double mfv = mfm * BarInput.Volume; + + // Update cumulative ADL only for new bars + if (BarInput.IsNew) + { + _cumulativeAdl += mfv; + } + + IsHot = _index >= WarmupPeriod; + return _cumulativeAdl; + } +} diff --git a/lib/volume/Adosc.cs b/lib/volume/Adosc.cs new file mode 100644 index 00000000..0d718fd2 --- /dev/null +++ b/lib/volume/Adosc.cs @@ -0,0 +1,138 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// ADOSC: Chaikin Accumulation/Distribution Oscillator +/// A momentum indicator that measures the strength of accumulation/distribution by combining +/// price and volume with moving averages. It helps identify potential trend reversals and +/// buying/selling pressure. +/// +/// +/// The ADOSC calculation process: +/// 1. Calculate ADL (Accumulation/Distribution Line) +/// a. Money Flow Multiplier = ((Close - Low) - (High - Close)) / (High - Low) +/// b. Money Flow Volume = MFM × Volume +/// c. ADL = Previous ADL + MFV +/// 2. Calculate two EMAs of ADL values +/// 3. Subtract longer EMA from shorter EMA +/// +/// Key characteristics: +/// - Volume-weighted measure +/// - Oscillates around zero +/// - Uses two different time periods +/// - Default periods are 3 and 10 days +/// - Shows momentum of money flow +/// +/// Formula: +/// MFM = ((Close - Low) - (High - Close)) / (High - Low) +/// MFV = MFM × Volume +/// ADL = Previous ADL + MFV +/// ADOSC = EMA(ADL, shortPeriod) - EMA(ADL, longPeriod) +/// +/// Market Applications: +/// - Trend confirmation +/// - Divergence analysis +/// - Volume/price relationship +/// - Support/resistance levels +/// - Market reversals +/// +/// Sources: +/// Marc Chaikin - Original development +/// https://www.investopedia.com/terms/c/chaikinoscillator.asp +/// +/// Note: Positive values indicate buying pressure, while negative values indicate selling pressure +/// + +[SkipLocalsInit] +public sealed class Adosc : AbstractBase +{ + private readonly int _longPeriod; + private double _cumulativeAdl; + private double _shortEma; + private double _longEma; + private readonly double _shortAlpha; + private readonly double _longAlpha; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Adosc(int shortPeriod = 3, int longPeriod = 10) + { + _longPeriod = longPeriod; + WarmupPeriod = longPeriod; // Need longer period for EMA calculation + Name = $"ADOSC({shortPeriod},{_longPeriod})"; + _shortAlpha = 2.0 / (shortPeriod + 1); + _longAlpha = 2.0 / (longPeriod + 1); + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Adosc(object source, int shortPeriod = 3, int longPeriod = 10) : this(shortPeriod, longPeriod) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _cumulativeAdl = 0; + _shortEma = 0; + _longEma = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateMoneyFlowMultiplier(double close, double high, double low) + { + double range = high - low; + if (range > 0) + { + return ((close - low) - (high - close)) / range; + } + return 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Calculate Money Flow Multiplier + double mfm = CalculateMoneyFlowMultiplier(BarInput.Close, BarInput.High, BarInput.Low); + + // Calculate Money Flow Volume + double mfv = mfm * BarInput.Volume; + + // Update cumulative ADL + _cumulativeAdl += mfv; + + // Calculate EMAs + if (_index <= _longPeriod) + { + // Initialize EMAs + _shortEma = _cumulativeAdl; + _longEma = _cumulativeAdl; + return 0; + } + + // Update EMAs + _shortEma = (_shortAlpha * _cumulativeAdl) + ((1 - _shortAlpha) * _shortEma); + _longEma = (_longAlpha * _cumulativeAdl) + ((1 - _longAlpha) * _longEma); + + // Calculate ADOSC + double adosc = _shortEma - _longEma; + + IsHot = _index >= WarmupPeriod; + return adosc; + } +} diff --git a/lib/volume/Aobv.cs b/lib/volume/Aobv.cs new file mode 100644 index 00000000..540ddd72 --- /dev/null +++ b/lib/volume/Aobv.cs @@ -0,0 +1,133 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// AOBV: Archer On-Balance Volume +/// A modified version of the traditional On-Balance Volume (OBV) indicator that uses a more +/// sophisticated method to determine buying and selling pressure. It considers both the +/// closing price and the price range to provide a more nuanced view of volume flow. +/// +/// +/// The AOBV calculation process: +/// 1. Determine price position within the day's range +/// 2. Apply volume based on price position: +/// - If close is in upper 1/3 of range: Add full volume +/// - If close is in middle 1/3 of range: Add/subtract half volume +/// - If close is in lower 1/3 of range: Subtract full volume +/// +/// Key characteristics: +/// - Volume-weighted measure +/// - Cumulative indicator +/// - No upper/lower bounds +/// - More nuanced than traditional OBV +/// - Considers price position in range +/// +/// Formula: +/// Range = High - Low +/// UpperThird = High - (Range / 3) +/// LowerThird = Low + (Range / 3) +/// If Close >= UpperThird: +/// AOBV = Previous AOBV + Volume +/// Else if Close <= LowerThird: +/// AOBV = Previous AOBV - Volume +/// Else: +/// If Close > Previous Close: +/// AOBV = Previous AOBV + (Volume / 2) +/// Else: +/// AOBV = Previous AOBV - (Volume / 2) +/// +/// Market Applications: +/// - Trend confirmation +/// - Volume analysis +/// - Price/volume divergence +/// - Support/resistance levels +/// - Market participation +/// +/// Sources: +/// Steve Archer - Original development +/// Technical Analysis of Stock Trends (Edwards, Magee) +/// +/// Note: Provides a more detailed analysis of volume flow than traditional OBV +/// + +[SkipLocalsInit] +public sealed class Aobv : AbstractBase +{ + private double _cumulativeAobv; + private double _prevClose; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Aobv() + { + WarmupPeriod = 1; + Name = "AOBV"; + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Aobv(object source) : this() + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _cumulativeAobv = 0; + _prevClose = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Skip first period to establish previous close + if (_index == 1) + { + _prevClose = BarInput.Close; + return 0; + } + + double range = BarInput.High - BarInput.Low; + if (range > 0) + { + double upperThird = BarInput.High - (range / 3); + double lowerThird = BarInput.Low + (range / 3); + + // Determine volume flow based on price position + if (BarInput.Close >= upperThird) + { + _cumulativeAobv += BarInput.Volume; + } + else if (BarInput.Close <= lowerThird) + { + _cumulativeAobv -= BarInput.Volume; + } + else + { + // In middle third, use half volume based on close comparison + _cumulativeAobv += (BarInput.Close > _prevClose) ? + (BarInput.Volume / 2) : -(BarInput.Volume / 2); + } + } + + _prevClose = BarInput.Close; + + IsHot = _index >= WarmupPeriod; + return _cumulativeAobv; + } +} diff --git a/lib/volume/Cmf.cs b/lib/volume/Cmf.cs new file mode 100644 index 00000000..2ac7abf6 --- /dev/null +++ b/lib/volume/Cmf.cs @@ -0,0 +1,129 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// CMF: Chaikin Money Flow +/// A volume-weighted technical indicator that measures the amount of Money Flow Volume (MFV) +/// over a specific period. Unlike ADL which is cumulative, CMF averages the Money Flow +/// Volume over a specified period. +/// +/// +/// The CMF calculation process: +/// 1. Calculates Money Flow Multiplier (MFM): +/// MFM = ((Close - Low) - (High - Close)) / (High - Low) +/// 2. Calculates Money Flow Volume (MFV): +/// MFV = MFM × Volume +/// 3. CMF = Sum(MFV) / Sum(Volume) over N periods +/// +/// Key characteristics: +/// - Oscillator between -1 and +1 +/// - Volume-weighted measure +/// - Non-cumulative indicator +/// - Default period is 20 days +/// +/// Formula: +/// MFM = ((Close - Low) - (High - Close)) / (High - Low) +/// MFV = MFM × Volume +/// CMF = Sum(MFV over N periods) / Sum(Volume over N periods) +/// +/// Market Applications: +/// - Trend confirmation +/// - Volume analysis +/// - Price/volume divergence +/// - Support/resistance levels +/// - Market participation +/// +/// Sources: +/// Marc Chaikin - Original development +/// https://www.investopedia.com/terms/c/chaikinmoneyflow.asp +/// +/// Note: Values above zero indicate buying pressure, while values below zero indicate selling pressure +/// + +[SkipLocalsInit] +public sealed class Cmf : AbstractBase +{ + private readonly int _period; + private readonly double[] _mfv; + private readonly double[] _volume; + private int _position; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cmf(int period = 20) + { + _period = period; + WarmupPeriod = period; + Name = $"CMF({_period})"; + _mfv = new double[period]; + _volume = new double[period]; + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cmf(object source, int period = 20) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _position = 0; + Array.Clear(_mfv, 0, _mfv.Length); + Array.Clear(_volume, 0, _volume.Length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateMoneyFlowMultiplier(double close, double high, double low) + { + double range = high - low; + if (range > 0) + { + return ((close - low) - (high - close)) / range; + } + return 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Calculate Money Flow Multiplier + double mfm = CalculateMoneyFlowMultiplier(BarInput.Close, BarInput.High, BarInput.Low); + + // Calculate Money Flow Volume + double currentMfv = mfm * BarInput.Volume; + + // Update circular buffers + _mfv[_position] = currentMfv; + _volume[_position] = BarInput.Volume; + _position = (_position + 1) % _period; + + // Calculate CMF + double sumMfv = 0; + double sumVolume = 0; + for (int i = 0; i < _period; i++) + { + sumMfv += _mfv[i]; + sumVolume += _volume[i]; + } + + double cmf = Math.Abs(sumVolume) > double.Epsilon ? sumMfv / sumVolume : 0; + IsHot = _index >= WarmupPeriod; + return cmf; + } +} diff --git a/lib/volume/Eom.cs b/lib/volume/Eom.cs new file mode 100644 index 00000000..0c2af40b --- /dev/null +++ b/lib/volume/Eom.cs @@ -0,0 +1,132 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// EOM: Ease of Movement +/// A volume-based technical indicator that relates price change to volume, showing the +/// relationship between price change and volume. It emphasizes days where price changes +/// are accomplished with minimal volume and minimizes days where large volume generates +/// small price changes. +/// +/// +/// The EOM calculation process: +/// 1. Calculate the distance moved: +/// Distance = ((High + Low)/2 - (Prior High + Prior Low)/2) +/// 2. Calculate the Box Ratio: +/// BoxRatio = Volume / (High - Low) +/// 3. Calculate single-period EMV: +/// EMV = Distance / BoxRatio +/// 4. Smooth EMV using simple moving average (optional) +/// +/// Key characteristics: +/// - Volume-weighted measure +/// - Oscillates around zero +/// - Shows ease of price movement +/// - Default period is 14 days +/// +/// Formula: +/// Distance = ((H + L)/2 - (pH + pL)/2) +/// BoxRatio = Volume / (High - Low) +/// EMV = Distance / BoxRatio +/// EOM = SMA(EMV, period) +/// +/// Market Applications: +/// - Trend strength analysis +/// - Volume/price relationship +/// - Support/resistance breakouts +/// - Market momentum +/// - Divergence identification +/// +/// Sources: +/// Richard W. Arms Jr. - Original development +/// https://www.investopedia.com/terms/e/easeofmovement.asp +/// +/// Note: Positive values suggest prices are rising with light volume (bullish), +/// while negative values suggest prices are falling with light volume (bearish) +/// + +[SkipLocalsInit] +public sealed class Eom : AbstractBase +{ + private readonly int _period; + private readonly double[] _emv; + private int _position; + private double _prevMidpoint; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Eom(int period = 14) + { + _period = period; + WarmupPeriod = period + 1; // Need one extra period for previous midpoint + Name = $"EOM({_period})"; + _emv = new double[period]; + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Eom(object source, int period = 14) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _position = 0; + _prevMidpoint = 0; + Array.Clear(_emv, 0, _emv.Length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + double midpoint = (BarInput.High + BarInput.Low) / 2; + double boxRatio = BarInput.Volume / (BarInput.High - BarInput.Low + double.Epsilon); // Avoid division by zero + + // Skip first period to establish previous midpoint + if (_index == 1) + { + _prevMidpoint = midpoint; + return 0; + } + + // Calculate distance moved + double distance = midpoint - _prevMidpoint; + + // Calculate EMV for this period + double emv = distance / boxRatio * 10000; // Multiply by 10000 to make values more readable + + // Store in circular buffer + _emv[_position] = emv; + _position = (_position + 1) % _period; + + // Calculate EOM (simple moving average of EMV) + double sum = 0; + for (int i = 0; i < _period; i++) + { + sum += _emv[i]; + } + double eom = sum / _period; + + // Store current midpoint for next calculation + _prevMidpoint = midpoint; + + IsHot = _index >= WarmupPeriod; + return eom; + } +} diff --git a/lib/volume/Kvo.cs b/lib/volume/Kvo.cs new file mode 100644 index 00000000..4e49c5b4 --- /dev/null +++ b/lib/volume/Kvo.cs @@ -0,0 +1,141 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// KVO: Klinger Volume Oscillator +/// A volume-based technical indicator that compares volume to price movement to identify +/// long-term trends and potential reversals. It helps determine the long-term money flow +/// while remaining sensitive to short-term fluctuations. +/// +/// +/// The KVO calculation process: +/// 1. Calculate Trend: +/// Trend = Current DM > Previous DM ? +1 : -1 +/// 2. Calculate Volume Force (VF): +/// VF = Volume * abs(ROC) * Trend * 100 +/// 3. Calculate two EMAs of VF and their difference: +/// Signal = EMA(VF, shortPeriod) - EMA(VF, longPeriod) +/// +/// Key characteristics: +/// - Volume-weighted measure +/// - Oscillates around zero +/// - Uses two different time periods +/// - Default periods are 34 and 55 days +/// - Shows volume force and price direction +/// +/// Formula: +/// DM = (H + L + C) / 3 +/// Trend = DM > Previous DM ? +1 : -1 +/// VF = Volume * abs(ROC) * Trend * 100 +/// KVO = EMA(VF, shortPeriod) - EMA(VF, longPeriod) +/// +/// Market Applications: +/// - Trend confirmation +/// - Divergence analysis +/// - Volume/price relationship +/// - Support/resistance levels +/// - Market reversals +/// +/// Sources: +/// Stephen Klinger - Original development +/// https://www.investopedia.com/terms/k/klingeroscillator.asp +/// +/// Note: Positive values indicate buying pressure, while negative values indicate selling pressure +/// + +[SkipLocalsInit] +public sealed class Kvo : AbstractBase +{ + private readonly int _longPeriod; + private double _prevDm; + private double _shortEma; + private double _longEma; + private readonly double _shortAlpha; + private readonly double _longAlpha; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Kvo(int shortPeriod = 34, int longPeriod = 55) + { + _longPeriod = longPeriod; + WarmupPeriod = longPeriod + 1; // Need one extra period for previous DM + Name = $"KVO({shortPeriod},{_longPeriod})"; + _shortAlpha = 2.0 / (shortPeriod + 1); + _longAlpha = 2.0 / (longPeriod + 1); + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Kvo(object source, int shortPeriod = 34, int longPeriod = 55) : this(shortPeriod, longPeriod) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prevDm = 0; + _shortEma = 0; + _longEma = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Calculate Daily Mean + double dm = (BarInput.High + BarInput.Low + BarInput.Close) / 3; + + // Skip first period to establish previous DM + if (_index == 1) + { + _prevDm = dm; + return 0; + } + + // Calculate Trend + int trend = dm > _prevDm ? 1 : -1; + + // Calculate Rate of Change + double roc = Math.Abs(dm - _prevDm) / _prevDm; + + // Calculate Volume Force + double vf = BarInput.Volume * roc * trend * 100; + + // Calculate EMAs + if (_index <= _longPeriod) + { + // Initialize EMAs + _shortEma = vf; + _longEma = vf; + } + else + { + // Update EMAs + _shortEma = (_shortAlpha * vf) + ((1 - _shortAlpha) * _shortEma); + _longEma = (_longAlpha * vf) + ((1 - _longAlpha) * _longEma); + } + + // Store current DM for next calculation + _prevDm = dm; + + // Calculate KVO + double kvo = _shortEma - _longEma; + + IsHot = _index >= WarmupPeriod; + return kvo; + } +} diff --git a/lib/volume/_list.md b/lib/volume/_list.md index 0b99daa1..86ec2225 100644 --- a/lib/volume/_list.md +++ b/lib/volume/_list.md @@ -1,9 +1,11 @@ -ADL - Chaikin Accumulation Distribution Line -ADOSC - Chaikin Accumulation Distribution Oscillator -AOBV - Archer On-Balance Volume -CMF - Chaikin Money Flow -EOM - Ease of Movement -KVO - Klinger Volume Oscillator +# Volume indicators + +✔️ ADL - Chaikin Accumulation Distribution Line +✔️ ADOSC - Chaikin Accumulation Distribution Oscillator +✔️ AOBV - Archer On-Balance Volume +✔️ CMF - Chaikin Money Flow +✔️ EOM - Ease of Movement +✔️ KVO - Klinger Volume Oscillator MFI - Money Flow Index NVI - Negative Volume Index OBV - On-Balance Volume From a99ff404b07b5cbaf94e103de3ef4594b8b699fd Mon Sep 17 00:00:00 2001 From: Miha Kralj Date: Tue, 29 Oct 2024 21:16:57 -0700 Subject: [PATCH 08/17] feat: add AC (Accelerator) and AO (Awesome) oscillator indicators --- Tests/test_updates_oscillators.cs | 32 ++++++++++++++ lib/oscillators/Ac.cs | 71 +++++++++++++++++++++++++++++++ lib/oscillators/Ao.cs | 71 +++++++++++++++++++++++++++++++ lib/oscillators/_list.md | 4 +- 4 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 lib/oscillators/Ac.cs create mode 100644 lib/oscillators/Ao.cs diff --git a/Tests/test_updates_oscillators.cs b/Tests/test_updates_oscillators.cs index 297f0ebd..ae2574a5 100644 --- a/Tests/test_updates_oscillators.cs +++ b/Tests/test_updates_oscillators.cs @@ -61,4 +61,36 @@ public class OscillatorsUpdateTests Assert.Equal(initialValue, finalValue, precision); } + + [Fact] + public void Ao_Update() + { + var indicator = new Ao(); + TBar r = new(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TBar(DateTime.Now, GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), 1000, IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Ac_Update() + { + var indicator = new Ac(); + TBar r = new(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TBar(DateTime.Now, GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), 1000, IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } } diff --git a/lib/oscillators/Ac.cs b/lib/oscillators/Ac.cs new file mode 100644 index 00000000..f298d560 --- /dev/null +++ b/lib/oscillators/Ac.cs @@ -0,0 +1,71 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// AC: Acceleration/Deceleration Oscillator +/// A momentum indicator that measures the acceleration and deceleration of the current driving force. +/// It is derived from the Awesome Oscillator (AO) and helps identify potential trend reversals. +/// +/// +/// The AC calculation process: +/// 1. Calculate the Awesome Oscillator (AO) +/// 2. Calculate a 5-period simple moving average of the AO +/// 3. Subtract the 5-period SMA from the current AO value +/// +/// Key characteristics: +/// - Oscillates above and below zero +/// - Measures the acceleration/deceleration of market driving force +/// - Positive values indicate increasing momentum +/// - Negative values indicate decreasing momentum +/// - Can be used to identify potential trend reversals +/// +/// Formula: +/// AC = AO - SMA(AO, 5) +/// +/// Sources: +/// Bill Williams - "Trading Chaos" (1995) +/// https://www.investopedia.com/terms/a/ac.asp +/// + +[SkipLocalsInit] +public sealed class Ac : AbstractBase +{ + private readonly Ao _ao; + private readonly Sma _sma5; + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Ac(object source) : this() + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Ac() + { + _ao = new Ao(); + _sma5 = new Sma(5); + WarmupPeriod = 39; // AO requires 34 periods + 5 for AC's SMA + Name = "AC"; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + var ao = _ao.Calc(BarInput, BarInput.IsNew); + _sma5.Calc(ao, BarInput.IsNew); + + return ao - _sma5.Value; + } +} diff --git a/lib/oscillators/Ao.cs b/lib/oscillators/Ao.cs new file mode 100644 index 00000000..97c39ea0 --- /dev/null +++ b/lib/oscillators/Ao.cs @@ -0,0 +1,71 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// AO: Awesome Oscillator +/// A momentum indicator that reflects the precise changes in the market driving force. +/// It is used to affirm trends or to anticipate possible reversals. +/// +/// +/// The AO calculation process: +/// 1. Calculates the 5-period simple moving average of the HL2 (High+Low)/2 values. +/// 2. Calculates the 34-period simple moving average of the HL2 (High+Low)/2 values. +/// 3. Subtracts the 34-period SMA from the 5-period SMA. +/// +/// Key characteristics: +/// - Oscillates above and below zero +/// - Positive values indicate bullish momentum +/// - Negative values indicate bearish momentum +/// - Crosses above zero suggest buying opportunities +/// - Crosses below zero suggest selling opportunities +/// +/// Formula: +/// AO = SMA(HL2, 5) - SMA(HL2, 34) +/// +/// Sources: +/// Bill Williams - "Trading Chaos" (1995) +/// https://www.investopedia.com/terms/a/awesomeoscillator.asp +/// + +[SkipLocalsInit] +public sealed class Ao : AbstractBase +{ + private readonly Sma _sma5; + private readonly Sma _sma34; + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Ao(object source) : this() + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Ao() + { + _sma5 = new Sma(5); + _sma34 = new Sma(34); + WarmupPeriod = 34; + Name = "AO"; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + _sma5.Calc(BarInput.HL2, BarInput.IsNew); + _sma34.Calc(BarInput.HL2, BarInput.IsNew); + + return _sma5.Value - _sma34.Value; + } +} diff --git a/lib/oscillators/_list.md b/lib/oscillators/_list.md index 618c07b3..b779856e 100644 --- a/lib/oscillators/_list.md +++ b/lib/oscillators/_list.md @@ -1,7 +1,7 @@ # Oscillators indicators -AC - Acceleration Oscillator -AO - Awesome Oscillator +✔️ AC - Acceleration Oscillator +✔️ AO - Awesome Oscillator AROON - Aroon oscillator BOP - Balance of Power CCI - Commodity Channel Index From 38793acc5760e6de6eecf57ce24ef5f0e6cd9343 Mon Sep 17 00:00:00 2001 From: Miha Kralj Date: Wed, 30 Oct 2024 07:48:42 -0700 Subject: [PATCH 09/17] feat: implemented new indicators Momentum: - DMX (Jurik Directional Movement Index) - MOM (Momentum) - PMO (Price Momentum Oscillator) - PO (Price Oscillator) - PPO (Percentage Price Oscillator) - PRS (Price Relative Strength) - ROC (Rate of Change) - VEL (Jurik Signal Velocity) Oscillators: - AC (Acceleration Oscillator) - AO (Awesome Oscillator) - RSX (Jurik Trend Strength Index) --- Tests/test_updates_momentum.cs | 125 +++++++++++++++++++++++ lib/momentum/Dmx.cs | 174 +++++++++++++++++++++++++++++++++ lib/momentum/Mom.cs | 75 ++++++++++++++ lib/momentum/Pmo.cs | 102 +++++++++++++++++++ lib/momentum/Po.cs | 80 +++++++++++++++ lib/momentum/Ppo.cs | 85 ++++++++++++++++ lib/momentum/Prs.cs | 84 ++++++++++++++++ lib/momentum/Roc.cs | 80 +++++++++++++++ lib/momentum/Vel.cs | 89 +++++++++++++++++ lib/momentum/_list.md | 18 ++-- 10 files changed, 903 insertions(+), 9 deletions(-) create mode 100644 lib/momentum/Dmx.cs create mode 100644 lib/momentum/Mom.cs create mode 100644 lib/momentum/Pmo.cs create mode 100644 lib/momentum/Po.cs create mode 100644 lib/momentum/Ppo.cs create mode 100644 lib/momentum/Prs.cs create mode 100644 lib/momentum/Roc.cs create mode 100644 lib/momentum/Vel.cs diff --git a/Tests/test_updates_momentum.cs b/Tests/test_updates_momentum.cs index a94d2386..3ca93dcc 100644 --- a/Tests/test_updates_momentum.cs +++ b/Tests/test_updates_momentum.cs @@ -88,4 +88,129 @@ public class MomentumUpdateTests Assert.Equal(initialValue, finalValue, precision); } + + [Fact] + public void Dmx_Update() + { + var indicator = new Dmx(period: 14); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Pmo_Update() + { + var indicator = new Pmo(period1: 35, period2: 20); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Po_Update() + { + var indicator = new Po(fastPeriod: 10, slowPeriod: 21); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Ppo_Update() + { + var indicator = new Ppo(fastPeriod: 12, slowPeriod: 26); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Prs_Update() + { + var indicator = new Prs(); + indicator.SetBenchmark(ReferenceValue); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.SetBenchmark(GetRandomDouble() + 100); // Ensure positive benchmark + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); + } + + indicator.SetBenchmark(ReferenceValue); + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Roc_Update() + { + var indicator = new Roc(period: 12); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble() + 100, IsNew: false)); // Ensure positive prices + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Mom_Update() + { + var indicator = new Mom(period: 10); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Vel_Update() + { + var indicator = new Vel(period: 10); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } } diff --git a/lib/momentum/Dmx.cs b/lib/momentum/Dmx.cs new file mode 100644 index 00000000..3f70113f --- /dev/null +++ b/lib/momentum/Dmx.cs @@ -0,0 +1,174 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// DMX: Enhanced Directional Movement Index using JMA smoothing +/// An improvement over the traditional DMI indicator that uses Jurik Moving Average (JMA) +/// for smoothing instead of Wilder's moving average. This enhancement provides better +/// noise reduction while maintaining responsiveness to significant price movements. +/// +/// +/// The DMX calculation process: +/// 1. Calculate True Range (TR) +/// 2. Calculate +DM (Positive Directional Movement) +/// 3. Calculate -DM (Negative Directional Movement) +/// 4. Smooth TR, +DM, and -DM using JMA instead of Wilder's smoothing +/// 5. Calculate +DI and -DI as percentages +/// +/// Key improvements over DMI: +/// - Uses JMA's adaptive volatility-based smoothing +/// - Better noise reduction in the directional movement signals +/// - Maintains responsiveness to significant price movements +/// - Reduced lag through JMA's phase-shifting +/// +/// Formula: +/// TR = max(high-low, abs(high-prevClose), abs(low-prevClose)) +/// +DM = if(high-prevHigh > prevLow-low) then max(high-prevHigh, 0) else 0 +/// -DM = if(prevLow-low > high-prevHigh) then max(prevLow-low, 0) else 0 +/// +DI = 100 * JMA(+DM) / JMA(TR) +/// -DI = 100 * JMA(-DM) / JMA(TR) +/// +/// Sources: +/// Original DMI by J. Welles Wilder Jr. - "New Concepts in Technical Trading Systems" (1978) +/// Enhanced with JMA smoothing by Mark Jurik +/// + +[SkipLocalsInit] +public sealed class Dmx : AbstractBarBase +{ + private readonly Jma _smoothedTr; + private readonly Jma _smoothedPlusDm; + private readonly Jma _smoothedMinusDm; + private double _prevHigh, _prevLow, _prevClose; + private double _p_prevHigh, _p_prevLow, _p_prevClose; + private double _plusDi, _minusDi; + private const double ScalingFactor = 100.0; + private const int DefaultPeriod = 10; + private const int DefaultPhase = 100; + private const double DefaultFactor = 0.25; + + /// + /// Gets the most recent +DI value + /// + public double PlusDI => _plusDi; + + /// + /// Gets the most recent -DI value + /// + public double MinusDI => _minusDi; + + /// The number of periods used in the DMX calculation (default 14). + /// The phase for the JMA smoothing (default 0). + /// The factor for the JMA smoothing (default 0.45). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Dmx(int period = DefaultPeriod, int phase = DefaultPhase, double factor = DefaultFactor) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + _smoothedTr = new(period, phase, factor); + _smoothedPlusDm = new(period, phase, factor); + _smoothedMinusDm = new(period, phase, factor); + _index = 0; + WarmupPeriod = period * 2; // JMA needs more warmup periods than RMA + Name = $"DMX({period})"; + } + + /// The data source object that publishes updates. + /// The number of periods used in the DMX calculation. + /// The phase for the JMA smoothing. + /// The factor for the JMA smoothing. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Dmx(object source, int period, int phase = DefaultPhase, double factor = DefaultFactor) : this(period, phase, factor) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _index++; + _p_prevHigh = _prevHigh; + _p_prevLow = _prevLow; + _p_prevClose = _prevClose; + } + else + { + _prevHigh = _p_prevHigh; + _prevLow = _p_prevLow; + _prevClose = _p_prevClose; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateTrueRange(double high, double low, double prevClose) + { + double hl = high - low; + double hpc = Math.Abs(high - prevClose); + double lpc = Math.Abs(low - prevClose); + return Math.Max(hl, Math.Max(hpc, lpc)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double plusDm, double minusDm) CalculateDirectionalMovement( + double high, double low, double prevHigh, double prevLow) + { + double upMove = high - prevHigh; + double downMove = prevLow - low; + + double plusDm = 0.0; + double minusDm = 0.0; + + if (upMove > downMove && upMove > 0) + plusDm = upMove; + else if (downMove > upMove && downMove > 0) + minusDm = downMove; + + return (plusDm, minusDm); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + if (_index == 1) + { + _prevHigh = Input.High; + _prevLow = Input.Low; + _prevClose = Input.Close; + return 0.0; + } + + // Calculate True Range and Directional Movement + double tr = CalculateTrueRange(Input.High, Input.Low, _prevClose); + var (plusDm, minusDm) = CalculateDirectionalMovement( + Input.High, Input.Low, _prevHigh, _prevLow); + + // Update previous values + _prevHigh = Input.High; + _prevLow = Input.Low; + _prevClose = Input.Close; + + // Smooth the indicators using JMA + _smoothedTr.Calc(tr, Input.IsNew); + _smoothedPlusDm.Calc(plusDm, Input.IsNew); + _smoothedMinusDm.Calc(minusDm, Input.IsNew); + + // Calculate +DI and -DI + double smoothedTr = _smoothedTr.Value; + if (smoothedTr > 0) + { + _plusDi = ScalingFactor * _smoothedPlusDm.Value / smoothedTr; + _minusDi = ScalingFactor * _smoothedMinusDm.Value / smoothedTr; + return _plusDi - _minusDi; // Return the difference as main value + } + + _plusDi = 0.0; + _minusDi = 0.0; + return 0.0; + } +} diff --git a/lib/momentum/Mom.cs b/lib/momentum/Mom.cs new file mode 100644 index 00000000..56e86f33 --- /dev/null +++ b/lib/momentum/Mom.cs @@ -0,0 +1,75 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// Mom: Momentum +/// A basic momentum indicator that measures the change in price over a specified +/// period, helping identify the strength and speed of price movements. +/// +/// +/// The Momentum calculation process: +/// 1. Store historical prices in a circular buffer +/// 2. Calculate absolute difference between current and historical price +/// 3. No scaling factor applied to maintain raw price difference +/// +/// Key characteristics: +/// - Basic momentum measurement +/// - Shows absolute price changes +/// - Zero line crossovers signal trend changes +/// - Foundation for other momentum indicators +/// +/// Formula: +/// Mom = Price - PriceN +/// where PriceN is the price N periods ago +/// +/// Sources: +/// Technical Analysis of Financial Markets by John J. Murphy +/// Technical Analysis Using Multiple Timeframes by Brian Shannon +/// + +[SkipLocalsInit] +public sealed class Mom : AbstractBase +{ + private readonly CircularBuffer _priceBuffer; + private const int DefaultPeriod = 10; + + /// The lookback period for momentum calculation (default 10). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Mom(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + + _priceBuffer = new(period + 1); + WarmupPeriod = period; + Name = $"MOM({period})"; + } + + /// The data source object that publishes updates. + /// The lookback period for momentum calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Mom(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + _priceBuffer.Add(Input.Value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + if (_priceBuffer.Count < _priceBuffer.Capacity) + return 0.0; + + return Input.Value - _priceBuffer[0]; + } +} diff --git a/lib/momentum/Pmo.cs b/lib/momentum/Pmo.cs new file mode 100644 index 00000000..cd22df83 --- /dev/null +++ b/lib/momentum/Pmo.cs @@ -0,0 +1,102 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// PMO: Price Momentum Oscillator +/// A momentum indicator that uses exponential moving averages of ROC (Rate of Change) +/// to identify overbought and oversold conditions in price movements. +/// +/// +/// The PMO calculation process: +/// 1. Calculate ROC (Rate of Change) of closing prices +/// 2. Apply a first smoothing EMA to the ROC values +/// 3. Apply a second smoothing EMA to the result +/// 4. Multiply by a scaling factor for better visualization +/// +/// Key characteristics: +/// - Double-smoothed momentum indicator +/// - Helps identify overbought/oversold conditions +/// - Useful for trend confirmation and divergence analysis +/// - More responsive than traditional momentum oscillators +/// +/// Formula: +/// ROC = (Close - PrevClose) / PrevClose +/// Signal1 = EMA(ROC, Period1) +/// PMO = EMA(Signal1, Period2) * ScalingFactor +/// +/// Sources: +/// Developed by Carl Swenlin +/// Technical Analysis of Stocks and Commodities magazine +/// + +[SkipLocalsInit] +public sealed class Pmo : AbstractBase +{ + private readonly Ema _smoothing1; + private readonly Ema _smoothing2; + private double _prevClose; + private double _p_prevClose; + private const double ScalingFactor = 100.0; + private const int DefaultPeriod1 = 35; + private const int DefaultPeriod2 = 20; + + /// The first smoothing period (default 35). + /// The second smoothing period (default 20). + /// Thrown when either period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Pmo(int period1 = DefaultPeriod1, int period2 = DefaultPeriod2) + { + if (period1 < 1 || period2 < 1) + throw new ArgumentOutOfRangeException(nameof(period1)); + + _smoothing1 = new(period1); + _smoothing2 = new(period2); + _index = 0; + WarmupPeriod = period1 + period2; + Name = $"PMO({period1},{period2})"; + } + + /// The data source object that publishes updates. + /// The first smoothing period. + /// The second smoothing period. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Pmo(object source, int period1, int period2) : this(period1, period2) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _index++; + _p_prevClose = _prevClose; + } + else + { + _prevClose = _p_prevClose; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + if (_index == 1) + { + _prevClose = Input.Value; + return 0.0; + } + + // Calculate Rate of Change + double roc = (Input.Value - _prevClose) / _prevClose; + _prevClose = Input.Value; + + // Apply double smoothing + double signal1 = _smoothing1.Calc(roc, Input.IsNew); + return _smoothing2.Calc(signal1, Input.IsNew) * ScalingFactor; + } +} diff --git a/lib/momentum/Po.cs b/lib/momentum/Po.cs new file mode 100644 index 00000000..170096bd --- /dev/null +++ b/lib/momentum/Po.cs @@ -0,0 +1,80 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// PO: Price Oscillator +/// A momentum indicator that measures the difference between two moving averages +/// of different periods to identify price momentum and potential trend changes. +/// +/// +/// The PO calculation process: +/// 1. Calculate fast EMA of closing prices +/// 2. Calculate slow EMA of closing prices +/// 3. Calculate the difference between fast and slow EMAs +/// 4. Multiply by a scaling factor for better visualization +/// +/// Key characteristics: +/// - Measures momentum through moving average differences +/// - Helps identify trend direction and potential reversals +/// - Zero line crossovers signal trend changes +/// - Similar to MACD but more customizable periods +/// +/// Formula: +/// FastMA = EMA(Close, FastPeriod) +/// SlowMA = EMA(Close, SlowPeriod) +/// PO = (FastMA - SlowMA) * ScalingFactor +/// +/// Sources: +/// Technical Analysis of Financial Markets by John J. Murphy +/// + +[SkipLocalsInit] +public sealed class Po : AbstractBase +{ + private readonly Ema _fastEma; + private readonly Ema _slowEma; + private const double ScalingFactor = 1.0; + private const int DefaultFastPeriod = 10; + private const int DefaultSlowPeriod = 21; + + /// The fast EMA period (default 10). + /// The slow EMA period (default 21). + /// Thrown when either period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Po(int fastPeriod = DefaultFastPeriod, int slowPeriod = DefaultSlowPeriod) + { + if (fastPeriod < 1 || slowPeriod < 1) + throw new ArgumentOutOfRangeException(nameof(fastPeriod)); + if (fastPeriod >= slowPeriod) + throw new ArgumentException("Fast period must be less than slow period"); + + _fastEma = new(fastPeriod); + _slowEma = new(slowPeriod); + WarmupPeriod = slowPeriod; + Name = $"PO({fastPeriod},{slowPeriod})"; + } + + /// The data source object that publishes updates. + /// The fast EMA period. + /// The slow EMA period. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Po(object source, int fastPeriod, int slowPeriod) : this(fastPeriod, slowPeriod) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + // No state management needed for this indicator + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + double fastEma = _fastEma.Calc(Input.Value, Input.IsNew); + double slowEma = _slowEma.Calc(Input.Value, Input.IsNew); + return (fastEma - slowEma) * ScalingFactor; + } +} diff --git a/lib/momentum/Ppo.cs b/lib/momentum/Ppo.cs new file mode 100644 index 00000000..2c46c9b8 --- /dev/null +++ b/lib/momentum/Ppo.cs @@ -0,0 +1,85 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// PPO: Percentage Price Oscillator +/// A momentum indicator that shows the percentage difference between two moving averages +/// of different periods, helping identify price momentum and potential trend changes. +/// +/// +/// The PPO calculation process: +/// 1. Calculate fast EMA of closing prices +/// 2. Calculate slow EMA of closing prices +/// 3. Calculate the percentage difference between fast and slow EMAs +/// 4. Multiply by a scaling factor for better visualization +/// +/// Key characteristics: +/// - Measures momentum through percentage differences +/// - Normalized for comparison across different price levels +/// - Zero line crossovers signal trend changes +/// - Similar to MACD but expressed as a percentage +/// +/// Formula: +/// FastMA = EMA(Close, FastPeriod) +/// SlowMA = EMA(Close, SlowPeriod) +/// PPO = ((FastMA - SlowMA) / SlowMA) * 100 +/// +/// Sources: +/// Technical Analysis of Financial Markets by John J. Murphy +/// StockCharts.com Technical Indicators +/// + +[SkipLocalsInit] +public sealed class Ppo : AbstractBase +{ + private readonly Ema _fastEma; + private readonly Ema _slowEma; + private const double ScalingFactor = 100.0; + private const int DefaultFastPeriod = 12; + private const int DefaultSlowPeriod = 26; + + /// The fast EMA period (default 12). + /// The slow EMA period (default 26). + /// Thrown when either period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Ppo(int fastPeriod = DefaultFastPeriod, int slowPeriod = DefaultSlowPeriod) + { + if (fastPeriod < 1 || slowPeriod < 1) + throw new ArgumentOutOfRangeException(nameof(fastPeriod)); + if (fastPeriod >= slowPeriod) + throw new ArgumentException("Fast period must be less than slow period"); + + _fastEma = new(fastPeriod); + _slowEma = new(slowPeriod); + WarmupPeriod = slowPeriod; + Name = $"PPO({fastPeriod},{slowPeriod})"; + } + + /// The data source object that publishes updates. + /// The fast EMA period. + /// The slow EMA period. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Ppo(object source, int fastPeriod, int slowPeriod) : this(fastPeriod, slowPeriod) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + // No state management needed for this indicator + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + double fastEma = _fastEma.Calc(Input.Value, Input.IsNew); + double slowEma = _slowEma.Calc(Input.Value, Input.IsNew); + + if (Math.Abs(slowEma) <= double.Epsilon) + return 0.0; + + return ((fastEma - slowEma) / slowEma) * ScalingFactor; + } +} diff --git a/lib/momentum/Prs.cs b/lib/momentum/Prs.cs new file mode 100644 index 00000000..f2bbb682 --- /dev/null +++ b/lib/momentum/Prs.cs @@ -0,0 +1,84 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// PRS: Price Relative Strength +/// A momentum indicator that compares the performance of a security against a benchmark, +/// helping identify which is showing stronger relative momentum. +/// +/// +/// The PRS calculation process: +/// 1. Take the current price of the security +/// 2. Take the current price of the benchmark +/// 3. Calculate the ratio between them +/// 4. Multiply by a scaling factor for better visualization +/// +/// Key characteristics: +/// - Measures relative performance against a benchmark +/// - Helps identify market leaders and laggards +/// - Rising PRS indicates outperformance +/// - Falling PRS indicates underperformance +/// +/// Formula: +/// PRS = (Price / Benchmark) * 100 +/// +/// Sources: +/// Technical Analysis of Financial Markets by John J. Murphy +/// StockCharts.com Technical Indicators +/// + +[SkipLocalsInit] +public sealed class Prs : AbstractBase +{ + private const double ScalingFactor = 100.0; + private double _benchmark; + private double _p_benchmark; + + /// + /// Initializes a new instance of the PRS indicator + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Prs() + { + WarmupPeriod = 1; + Name = "PRS"; + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Prs(object source) : this() + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + /// + /// Sets the current benchmark value + /// + /// The benchmark value to compare against + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetBenchmark(double benchmark) + { + _benchmark = benchmark; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + _p_benchmark = _benchmark; + else + _benchmark = _p_benchmark; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + if (_benchmark <= double.Epsilon) + return 0.0; + + return (Input.Value / _benchmark) * ScalingFactor; + } +} diff --git a/lib/momentum/Roc.cs b/lib/momentum/Roc.cs new file mode 100644 index 00000000..f61070ec --- /dev/null +++ b/lib/momentum/Roc.cs @@ -0,0 +1,80 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// ROC: Rate of Change +/// A momentum indicator that measures the percentage change in price over a specified +/// period, helping identify the speed and strength of price movements. +/// +/// +/// The ROC calculation process: +/// 1. Store historical prices in a circular buffer +/// 2. Calculate percentage change between current and historical price +/// 3. Multiply by scaling factor for better visualization +/// +/// Key characteristics: +/// - Pure momentum indicator +/// - Oscillates around zero line +/// - Helps identify overbought/oversold conditions +/// - Useful for divergence analysis +/// +/// Formula: +/// ROC = ((Price - PriceN) / PriceN) * 100 +/// where PriceN is the price N periods ago +/// +/// Sources: +/// Technical Analysis of Financial Markets by John J. Murphy +/// Technical Analysis of Stock Trends by Robert D. Edwards and John Magee +/// + +[SkipLocalsInit] +public sealed class Roc : AbstractBase +{ + private readonly CircularBuffer _priceBuffer; + private const double ScalingFactor = 100.0; + private const int DefaultPeriod = 12; + + /// The lookback period for ROC calculation (default 12). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Roc(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + + _priceBuffer = new(period + 1); + WarmupPeriod = period; + Name = $"ROC({period})"; + } + + /// The data source object that publishes updates. + /// The lookback period for ROC calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Roc(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + _priceBuffer.Add(Input.Value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + if (_priceBuffer.Count < _priceBuffer.Capacity) + return 0.0; + + double oldPrice = _priceBuffer[0]; + if (oldPrice <= double.Epsilon) + return 0.0; + + return ((Input.Value - oldPrice) / oldPrice) * ScalingFactor; + } +} diff --git a/lib/momentum/Vel.cs b/lib/momentum/Vel.cs new file mode 100644 index 00000000..c0a8472e --- /dev/null +++ b/lib/momentum/Vel.cs @@ -0,0 +1,89 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// Vel: Velocity +/// An enhanced momentum indicator that applies Jurik Moving Average (JMA) smoothing +/// to the basic momentum calculation, providing better noise reduction while +/// maintaining responsiveness to significant price movements. +/// +/// +/// The Velocity calculation process: +/// 1. Calculate basic momentum (price difference) +/// 2. Apply JMA smoothing to the momentum values +/// 3. No scaling factor applied to maintain price-based units +/// +/// Key characteristics: +/// - Enhanced momentum measurement with JMA smoothing +/// - Better noise reduction than basic momentum +/// - Maintains responsiveness to significant moves +/// - Reduced lag through JMA's phase-shifting +/// +/// Formula: +/// Mom = Price - PriceN +/// Vel = JMA(Mom, period) +/// +/// Sources: +/// Enhanced with JMA smoothing by Mark Jurik +/// Technical Analysis of Financial Markets by John J. Murphy +/// + +[SkipLocalsInit] +public sealed class Vel : AbstractBase +{ + private readonly CircularBuffer _priceBuffer; + private readonly Jma _smoothing; + private const int DefaultPeriod = 10; + private const int DefaultPhase = 100; + private const double DefaultFactor = 0.25; + + /// The lookback period for velocity calculation (default 10). + /// The phase for the JMA smoothing (default 0). + /// The power factor for the JMA smoothing (default 2.0). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vel(int period = DefaultPeriod, int phase = DefaultPhase, double factor = DefaultFactor) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + + _priceBuffer = new(period + 1); + _smoothing = new(period, phase, factor); + WarmupPeriod = period * 2; // JMA needs more warmup periods + Name = $"VEL({period})"; + } + + /// The data source object that publishes updates. + /// The lookback period for velocity calculation. + /// The phase for the JMA smoothing. + /// The power factor for the JMA smoothing. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vel(object source, int period, int phase = DefaultPhase, double power = DefaultFactor) + : this(period, phase, power) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + _priceBuffer.Add(Input.Value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + if (_priceBuffer.Count < _priceBuffer.Capacity) + return 0.0; + + // Calculate basic momentum + double momentum = Input.Value - _priceBuffer[0]; + + // Apply JMA smoothing + return _smoothing.Calc(momentum, Input.IsNew); + } +} diff --git a/lib/momentum/_list.md b/lib/momentum/_list.md index 89739c87..1928a3d8 100644 --- a/lib/momentum/_list.md +++ b/lib/momentum/_list.md @@ -3,16 +3,16 @@ ✔️ ADX - Average Directional Movement Index ✔️ ADXR - Average Directional Movement Index Rating ✔️ APO - Absolute Price Oscillator -DMI - Directional Movement Index -DMX - Jurik Directional Movement Index +✔️ DMI - Directional Movement Index +✔️ DMX - Jurik Directional Movement Index DPO - Detrended Price Oscillator MACD - Moving Average Convergence/Divergence -MOM - Momentum -PMO - Price Momentum Oscillator -PO - Price Oscillator -PPO - Percentage Price Oscillator -PRS - Price Relative Strength -ROC - Rate of Change +✔️ MOM - Momentum +✔️ PMO - Price Momentum Oscillator +✔️ PO - Price Oscillator +✔️ PPO - Percentage Price Oscillator +✔️ PRS - Price Relative Strength +✔️ ROC - Rate of Change TRIX - 1-day ROC of TEMA -VEL - Jurik Signal Velocity +✔️ VEL - Jurik Signal Velocity VORTEX - Vortex Indicator From 06c6875970cbea5b9227d3fdb83b6176bb21aaf3 Mon Sep 17 00:00:00 2001 From: Miha Date: Wed, 30 Oct 2024 10:42:50 -0700 Subject: [PATCH 10/17] feat: Trix, Aroon --- Tests/test_updates_momentum.cs | 15 ++++ Tests/test_updates_oscillators.cs | 16 ++++ lib/_todolist.md | 142 ++++++++++++++++++++++++++++++ lib/momentum/Trix.cs | 87 ++++++++++++++++++ lib/momentum/_list.md | 4 +- lib/oscillators/Aroon.cs | 120 +++++++++++++++++++++++++ lib/oscillators/_list.md | 3 +- lib/patterns/_list.md | 11 +++ lib/statistics/_list.md | 1 + lib/volatility/_list.md | 3 +- lib/volume/_list.md | 1 + 11 files changed, 400 insertions(+), 3 deletions(-) create mode 100644 lib/_todolist.md create mode 100644 lib/momentum/Trix.cs create mode 100644 lib/oscillators/Aroon.cs create mode 100644 lib/patterns/_list.md diff --git a/Tests/test_updates_momentum.cs b/Tests/test_updates_momentum.cs index 3ca93dcc..7bfe4a92 100644 --- a/Tests/test_updates_momentum.cs +++ b/Tests/test_updates_momentum.cs @@ -199,6 +199,21 @@ public class MomentumUpdateTests Assert.Equal(initialValue, finalValue, precision); } + [Fact] + public void Trix_Update() + { + var indicator = new Trix(period: 18); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble() + 100, IsNew: false)); // Ensure positive prices + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + [Fact] public void Vel_Update() { diff --git a/Tests/test_updates_oscillators.cs b/Tests/test_updates_oscillators.cs index ae2574a5..56cdbe93 100644 --- a/Tests/test_updates_oscillators.cs +++ b/Tests/test_updates_oscillators.cs @@ -93,4 +93,20 @@ public class OscillatorsUpdateTests Assert.Equal(initialValue, finalValue, precision); } + + [Fact] + public void Aroon_Update() + { + var indicator = new Aroon(period: 25); + TBar r = new(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TBar(DateTime.Now, GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), 1000, IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } } diff --git a/lib/_todolist.md b/lib/_todolist.md new file mode 100644 index 00000000..035ce46b --- /dev/null +++ b/lib/_todolist.md @@ -0,0 +1,142 @@ +# Stock Indicators List + +## Common Indicators (Both Libraries) + +| Indicator Name | Skender Method | QuanTAlib Class | +|---------------|----------------|-----------------| +| ADL - Accumulation/Distribution Line | GetAdl | Adl | +| ADOSC - Accumulation/Distribution Oscillator | GetAdo | Adosc | +| ALMA - Arnaud Legoux Moving Average | GetAlma | Alma | +| AROON - Aroon Oscillator | GetAroon | Aroon | +| ADX - Average Directional Index | GetAdx | Adx | +| ATR - Average True Range | GetAtr | Atr | +| AO - Awesome Oscillator | GetAwesome | Ao | +| CMF - Chaikin Money Flow | GetCmf | Cmf | +| CMO - Chande Momentum Oscillator | GetCmo | Cmo | +| DEMA - Double Exponential Moving Average | GetDema | Dema | +| EOM - Ease of Movement | GetEom | Eom | +| EPMA - Endpoint Moving Average | GetEpma | Epma | +| EMA - Exponential Moving Average | GetEma | Ema | +| HTIT - Hilbert Transform Instantaneous Trendline | GetHtTrendline | Htit | +| HMA - Hull Moving Average | GetHma | Hma | +| KVO - Klinger Volume Oscillator | GetKvo | Kvo | +| OBV - On-Balance Volume | GetObv | Aobv | +| PMO - Price Momentum Oscillator | GetPmo | Pmo | +| PRS - Price Relative Strength | GetPrs | Prs | +| ROC - Rate of Change | GetRoc | Roc | +| RSI - Relative Strength Index | GetRsi | Rsi | +| SMA - Simple Moving Average | GetSma | Sma | +| SMMA - Smoothed Moving Average | GetSmma | Smma | +| SLOPE - Slope and Linear Regression | GetSlope | Slope | +| STDEV - Standard Deviation | GetStdDev | Stddev | +| TRIX - Triple EMA Oscillator | GetTrix | Trix | +| TEMA - Triple Exponential Moving Average | GetTema | Tema | +| WMA - Weighted Moving Average | GetWma | Wma | + +## Skender-Only Indicators + +| Indicator Name | Skender Method | +|---------------|----------------| +| ATRS - ATR Trailing Stop | GetAtrStop | +| BOP - Balance of Power | GetBop | +| BETA - Beta Coefficient | GetBeta | +| BB - Bollinger Bands | GetBollingerBands | +| CE - Chandelier Exit | GetChandelier | +| CHOP - Choppiness Index | GetChop | +| CCI - Commodity Channel Index | GetCci | +| CRSI - Connors RSI | GetConnorsRsi | +| CORR - Correlation Coefficient | GetCorrelation | +| DPO - Detrended Price Oscillator | GetDpo | +| DOJI - Doji Pattern | GetDoji | +| DC - Donchian Channel | GetDonchian | +| ER - Elder-Ray | GetElderRay | +| FISH - Fisher Transform | GetFisherTransform | +| FI - Force Index | GetForceIndex | +| FCB - Fractal Chaos Bands | GetFcb | +| GATOR - Gator Oscillator | GetGator | +| HA - Heikin-Ashi | GetHeikinAshi | +| HURST - Hurst Exponent | GetHurst | +| ICH - Ichimoku Cloud | GetIchimoku | +| KC - Keltner Channels | GetKeltner | +| MARU - Marubozu Pattern | GetMarubozu | +| MFI - Money Flow Index | GetMfi | +| MAE - Moving Average Envelopes | GetMaEnvelopes | +| PSAR - Parabolic SAR | GetParabolicSar | +| PP - Pivot Points | GetPivotPoints | +| PIV - Pivots | GetPivots | +| PVO - Price Volume Oscillator | GetPvo | +| RENKO-ATR - Renko Chart ATR | GetRenkoAtr | +| RENKO - Renko Chart Standard | GetRenko | +| RPP - Rolling Pivot Points | GetRollingPivots | +| STC - Schaff Trend Cycle | GetStc | +| SDC - Standard Deviation Channels | GetStdDevChannels | +| STARC - Starc Bands | GetStarcBands | +| SMI - Stochastic Momentum Index | GetSmi | +| STOCH - Stochastic Oscillator | GetStoch | +| STOCH-RSI - Stochastic RSI | GetStochRsi | +| ST - Supertrend | GetSuperTrend | +| TR - True Range | GetTr | +| TSI - True Strength Index | GetTsi | +| UI - Ulcer Index | GetUlcerIndex | +| UO - Ultimate Oscillator | GetUltimate | +| VSS - Volatility System/Stop | GetVolatilityStop | +| VWAP - Volume Weighted Average Price | GetVwap | +| VWMA - Volume Weighted Moving Average | GetVwma | +| VTX - Vortex Indicator | GetVortex | +| WAG - Williams Alligator | GetAlligator | +| WF - Williams Fractal | GetFractal | +| ZZ - Zig Zag | GetZigZag | + +## QuanTAlib-Only Indicators + +| Indicator Name | QuanTAlib Class | +|---------------|-----------------| +| AC - Acceleration Oscillator | Ac | +| AFIRMA - Adaptive Firman Moving Average | Afirma | +| APO - Absolute Price Oscillator | Apo | +| ADXR - ADX Rating | Adxr | +| CONV - Convolution Moving Average | Convolution | +| CURV - Curvature | Curvature | +| DMI - Directional Movement Index | Dmi | +| DMX - Directional Movement Extended | Dmx | +| DSMA - Double Smoothed Moving Average | Dsma | +| DWMA - Dynamic Weighted Moving Average | Dwma | +| ENT - Entropy | Entropy | +| FRAMA - Fractal Adaptive Moving Average | Frama | +| FWMA - Fibonacci Weighted Moving Average | Fwma | +| GMA - Gaussian Moving Average | Gma | +| HV - Historical Volatility | Hv | +| HWMA - Hybrid Weighted Moving Average | Hwma | +| JMA - Jurik Moving Average | Jma | +| JVOL - Jurik Volatility | Jvolty | +| KURT - Kurtosis | Kurtosis | +| KAMA - Kaufman Adaptive Moving Average | Kama | +| LTMA - Laguerre Time Moving Average | Ltma | +| MAX - Maximum Value | Max | +| MAAF - Median Adaptive Antifractal | Maaf | +| MAMA - Mesa Adaptive Moving Average | Mama | +| MGDI - McGinley Dynamic Indicator | Mgdi | +| MEDIAN - Median Value | Median | +| MIN - Minimum Value | Min | +| MMA - Modified Moving Average | Mma | +| MODE - Mode Value | Mode | +| MOM - Momentum | Mom | +| PCTL - Percentile | Percentile | +| PO - Price Oscillator | Po | +| PPO - Price Percentage Oscillator | Ppo | +| PWMA - Polynomial Weighted Moving Average | Pwma | +| QEMA - Quadratic Exponential Moving Average | Qema | +| REMA - Range-Normalized Exponential Moving Average | Rema | +| RSX - Relative Strength Extended | Rsx | +| RV - Realized Volatility | Rv | +| RVI - Relative Volatility Index | Rvi | +| RMA - Rolling Moving Average | Rma | +| SINEMA - Sine-Wave Exponential Moving Average | Sinema | +| SKEW - Skewness | Skew | +| T3 - Tillson T3 Moving Average | T3 | +| TRIMA - Triangular Moving Average | Trima | +| VAR - Variance | Variance | +| VIDYA - Variable Index Dynamic Average | Vidya | +| VEL - Velocity | Vel | +| ZLEMA - Zero-Lag Exponential Moving Average | Zlema | +| ZSCORE - Z-Score | Zscore | diff --git a/lib/momentum/Trix.cs b/lib/momentum/Trix.cs new file mode 100644 index 00000000..aa744ab6 --- /dev/null +++ b/lib/momentum/Trix.cs @@ -0,0 +1,87 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// TRIX: Triple Exponential Average Rate of Change +/// A momentum oscillator that shows the percentage rate of change of a triple exponentially +/// smoothed moving average. TRIX filters out insignificant price movements and helps identify +/// overbought/oversold conditions and divergences. +/// +/// +/// The TRIX calculation process: +/// 1. Calculate Triple Exponential Moving Average (TEMA) +/// 2. Calculate 1-day Rate of Change (ROC) of the TEMA +/// +/// Key characteristics: +/// - Combines trend-following and momentum in one indicator +/// - Filters out price movements deemed insignificant +/// - Oscillates around zero line +/// - Useful for identifying divergences +/// - Helps spot overbought/oversold conditions +/// +/// Formula: +/// TEMA = 3*EMA1 - 3*EMA2 + EMA3 +/// TRIX = ROC(TEMA, 1) = ((TEMA - TEMA_prev) / TEMA_prev) * 100 +/// +/// Sources: +/// Jack Hutson - "Technical Analysis of Stocks and Commodities" magazine, 1983 +/// John J. Murphy - "Technical Analysis of the Financial Markets" +/// + +[SkipLocalsInit] +public sealed class Trix : AbstractBase +{ + private readonly Tema _tema; + private readonly CircularBuffer _temaBuffer; + private const double ScalingFactor = 100.0; + private const int DefaultPeriod = 18; + + /// The lookback period for TEMA calculation (default 18). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Trix(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + + _tema = new(period); + _temaBuffer = new(2); // We only need current and previous TEMA values + WarmupPeriod = period + 1; // TEMA period + 1 for ROC + Name = $"TRIX({period})"; + } + + /// The data source object that publishes updates. + /// The lookback period for TEMA calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Trix(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + double temaValue = _tema.Calc(Input); + _temaBuffer.Add(temaValue); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + if (_temaBuffer.Count < _temaBuffer.Capacity) + return 0.0; + + double oldTema = _temaBuffer[0]; + if (oldTema <= double.Epsilon) + return 0.0; + + double currentTema = _temaBuffer[^1]; + return ((currentTema - oldTema) / oldTema) * ScalingFactor; + } +} diff --git a/lib/momentum/_list.md b/lib/momentum/_list.md index 1928a3d8..fe0e04cc 100644 --- a/lib/momentum/_list.md +++ b/lib/momentum/_list.md @@ -1,4 +1,5 @@ # Momentum indicators +Done: 12, Todo: 5 ✔️ ADX - Average Directional Movement Index ✔️ ADXR - Average Directional Movement Index Rating @@ -13,6 +14,7 @@ MACD - Moving Average Convergence/Divergence ✔️ PPO - Percentage Price Oscillator ✔️ PRS - Price Relative Strength ✔️ ROC - Rate of Change -TRIX - 1-day ROC of TEMA +TSI - True Strength Index +✔️ TRIX - 1-day ROC of TEMA ✔️ VEL - Jurik Signal Velocity VORTEX - Vortex Indicator diff --git a/lib/oscillators/Aroon.cs b/lib/oscillators/Aroon.cs new file mode 100644 index 00000000..cb1f6c20 --- /dev/null +++ b/lib/oscillators/Aroon.cs @@ -0,0 +1,120 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// AROON: Aroon Oscillator +/// A trend-following indicator that measures the strength of a trend and the likelihood +/// that the trend will continue. It consists of two lines (Aroon Up and Aroon Down) and +/// their difference forms the Aroon Oscillator. +/// +/// +/// The Aroon calculation process: +/// 1. Tracks the number of periods since the last highest high (Aroon Up) +/// 2. Tracks the number of periods since the last lowest low (Aroon Down) +/// 3. Normalizes both values to a 0-100 scale +/// 4. Calculates the difference (Aroon Oscillator) +/// +/// Key characteristics: +/// - Oscillates between -100 and +100 +/// - Positive values indicate uptrend +/// - Negative values indicate downtrend +/// - Zero line crossovers signal trend changes +/// - Extreme readings suggest strong trends +/// +/// Formula: +/// Aroon Up = ((period - days since highest high) / period) × 100 +/// Aroon Down = ((period - days since lowest low) / period) × 100 +/// Aroon Oscillator = Aroon Up - Aroon Down +/// +/// Sources: +/// Tushar Chande - "The New Technical Trader" (1994) +/// https://www.investopedia.com/terms/a/aroonoscillator.asp +/// +/// Note: Default period of 25 was recommended by Chande +/// + +[SkipLocalsInit] +public sealed class Aroon : AbstractBarBase +{ + private readonly CircularBuffer _highPrices; + private readonly CircularBuffer _lowPrices; + private const double ScalingFactor = 100.0; + private const int DefaultPeriod = 25; + + /// The number of periods used in the Aroon calculation (default 25). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Aroon(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + + _highPrices = new(period); + _lowPrices = new(period); + _index = 0; + WarmupPeriod = period; + Name = $"AROON({period})"; + } + + /// The data source object that publishes updates. + /// The number of periods used in the Aroon calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Aroon(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _index++; + _highPrices.Add(Input.High); + _lowPrices.Add(Input.Low); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateAroonLine(int period, int daysSince) + { + return ((period - daysSince) * ScalingFactor) / period; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + if (_index < WarmupPeriod) + return double.NaN; + + // Find highest high and lowest low positions + int highestIndex = 0; + int lowestIndex = 0; + double highestHigh = _highPrices[0]; + double lowestLow = _lowPrices[0]; + + for (int i = 1; i < _highPrices.Count; i++) + { + if (_highPrices[i] > highestHigh) + { + highestHigh = _highPrices[i]; + highestIndex = i; + } + if (_lowPrices[i] < lowestLow) + { + lowestLow = _lowPrices[i]; + lowestIndex = i; + } + } + + // Calculate Aroon Up and Down + double aroonUp = CalculateAroonLine(_highPrices.Count, highestIndex); + double aroonDown = CalculateAroonLine(_lowPrices.Count, lowestIndex); + + // Return Aroon Oscillator + return aroonUp - aroonDown; + } +} diff --git a/lib/oscillators/_list.md b/lib/oscillators/_list.md index b779856e..99e19436 100644 --- a/lib/oscillators/_list.md +++ b/lib/oscillators/_list.md @@ -1,8 +1,9 @@ # Oscillators indicators +Done: 6, Todo: 23 ✔️ AC - Acceleration Oscillator ✔️ AO - Awesome Oscillator -AROON - Aroon oscillator +✔️ AROON - Aroon oscillator BOP - Balance of Power CCI - Commodity Channel Index CFO - Chande Forcast Oscillator diff --git a/lib/patterns/_list.md b/lib/patterns/_list.md new file mode 100644 index 00000000..8b54056f --- /dev/null +++ b/lib/patterns/_list.md @@ -0,0 +1,11 @@ +# Pattern indicators +Done: 0, Todo: 8 + +DOJI - Doji Candlestick Pattern +ER - Elder Ray Pattern +MARU - Marubozu Candlestick Pattern +PIV - Pivot Points +PP - Price Pivots +RPP - Rolling Pivot Points +WF - Williams Fractal +ZZ - Zig Zag Pattern diff --git a/lib/statistics/_list.md b/lib/statistics/_list.md index 84fa4b37..627de7c8 100644 --- a/lib/statistics/_list.md +++ b/lib/statistics/_list.md @@ -1,4 +1,5 @@ # Statistics indicators +Done: 13, Todo: 6 BETA - Beta coefficient CORR - Correlation Coefficient diff --git a/lib/volatility/_list.md b/lib/volatility/_list.md index 1ec6089e..93598c02 100644 --- a/lib/volatility/_list.md +++ b/lib/volatility/_list.md @@ -1,4 +1,5 @@ # Volatility indicators +Done: 6, Todo: 25 ADR - Average Daily Range AP - Andrew's Pitchfork @@ -33,4 +34,4 @@ VC - Volatility Cone VOV - Volatility of Volatility VR - Volatility Ratio VS - Volatility Stop -YZV - Yang-Zhang Volatility \ No newline at end of file +YZV - Yang-Zhang Volatility diff --git a/lib/volume/_list.md b/lib/volume/_list.md index 86ec2225..54149915 100644 --- a/lib/volume/_list.md +++ b/lib/volume/_list.md @@ -1,4 +1,5 @@ # Volume indicators +Done: 6, Todo: 12 ✔️ ADL - Chaikin Accumulation Distribution Line ✔️ ADOSC - Chaikin Accumulation Distribution Oscillator From 6231bab9e528bea2dc1703afaefa71a7906ab170 Mon Sep 17 00:00:00 2001 From: Miha Date: Wed, 30 Oct 2024 13:45:36 -0700 Subject: [PATCH 11/17] feat: Dpo, Tsi, Vortex, Bpp, Cci, Cfo, Tr, Ui, Vc, Vov, Vr, Vs, Mfi, Nvi, Obv, Pvi, Pvo, Pvol, Pvr, Pvt, Tvi --- Tests/test_updates_momentum.cs | 46 ++++ Tests/test_updates_oscillators.cs | 47 ++++ Tests/test_updates_volatility.cs | 96 ++++++++ Tests/test_updates_volume.cs | 163 +++++++++++++ docs/indicators/indicators.md | 390 ++++++++++++++++-------------- lib/averages/_list.md | 4 +- lib/momentum/Dpo.cs | 112 +++++++++ lib/momentum/Tsi.cs | 151 ++++++++++++ lib/momentum/Vortex.cs | 162 +++++++++++++ lib/momentum/_list.md | 12 +- lib/oscillators/Bop.cs | 66 +++++ lib/oscillators/Cci.cs | 101 ++++++++ lib/oscillators/Cfo.cs | 117 +++++++++ lib/oscillators/_list.md | 12 +- lib/patterns/_list.md | 8 +- lib/statistics/_list.md | 10 +- lib/volatility/Tr.cs | 102 ++++++++ lib/volatility/Ui.cs | 114 +++++++++ lib/volatility/Vc.cs | 164 +++++++++++++ lib/volatility/Vov.cs | 131 ++++++++++ lib/volatility/Vr.cs | 135 +++++++++++ lib/volatility/Vs.cs | 155 ++++++++++++ lib/volatility/_list.md | 27 ++- lib/volatility/todo.md | 29 --- lib/volume/Mfi.cs | 141 +++++++++++ lib/volume/Nvi.cs | 114 +++++++++ lib/volume/Obv.cs | 117 +++++++++ lib/volume/Pvi.cs | 113 +++++++++ lib/volume/Pvo.cs | 111 +++++++++ lib/volume/Pvol.cs | 108 +++++++++ lib/volume/Pvr.cs | 109 +++++++++ lib/volume/Pvt.cs | 105 ++++++++ lib/volume/Tvi.cs | 112 +++++++++ lib/volume/_list.md | 21 +- 34 files changed, 3151 insertions(+), 254 deletions(-) create mode 100644 lib/momentum/Dpo.cs create mode 100644 lib/momentum/Tsi.cs create mode 100644 lib/momentum/Vortex.cs create mode 100644 lib/oscillators/Bop.cs create mode 100644 lib/oscillators/Cci.cs create mode 100644 lib/oscillators/Cfo.cs create mode 100644 lib/volatility/Tr.cs create mode 100644 lib/volatility/Ui.cs create mode 100644 lib/volatility/Vc.cs create mode 100644 lib/volatility/Vov.cs create mode 100644 lib/volatility/Vr.cs create mode 100644 lib/volatility/Vs.cs delete mode 100644 lib/volatility/todo.md create mode 100644 lib/volume/Mfi.cs create mode 100644 lib/volume/Nvi.cs create mode 100644 lib/volume/Obv.cs create mode 100644 lib/volume/Pvi.cs create mode 100644 lib/volume/Pvo.cs create mode 100644 lib/volume/Pvol.cs create mode 100644 lib/volume/Pvr.cs create mode 100644 lib/volume/Pvt.cs create mode 100644 lib/volume/Tvi.cs diff --git a/Tests/test_updates_momentum.cs b/Tests/test_updates_momentum.cs index 7bfe4a92..7338cbdd 100644 --- a/Tests/test_updates_momentum.cs +++ b/Tests/test_updates_momentum.cs @@ -105,6 +105,21 @@ public class MomentumUpdateTests Assert.Equal(initialValue, finalValue, precision); } + [Fact] + public void Dpo_Update() + { + var indicator = new Dpo(period: 20); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + [Fact] public void Pmo_Update() { @@ -214,6 +229,21 @@ public class MomentumUpdateTests Assert.Equal(initialValue, finalValue, precision); } + [Fact] + public void Tsi_Update() + { + var indicator = new Tsi(firstPeriod: 25, secondPeriod: 13); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble() + 100, IsNew: false)); // Ensure positive prices + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + [Fact] public void Vel_Update() { @@ -228,4 +258,20 @@ public class MomentumUpdateTests Assert.Equal(initialValue, finalValue, precision); } + + [Fact] + public void Vortex_Update() + { + var indicator = new Vortex(period: 14); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } } diff --git a/Tests/test_updates_oscillators.cs b/Tests/test_updates_oscillators.cs index 56cdbe93..ba552e4e 100644 --- a/Tests/test_updates_oscillators.cs +++ b/Tests/test_updates_oscillators.cs @@ -109,4 +109,51 @@ public class OscillatorsUpdateTests Assert.Equal(initialValue, finalValue, precision); } + + [Fact] + public void Bop_Update() + { + var indicator = new Bop(); + TBar r = new(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TBar(DateTime.Now, GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), 1000, IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Cci_Update() + { + var indicator = new Cci(period: 20); + TBar r = new(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TBar(DateTime.Now, GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), 1000, IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Cfo_Update() + { + var indicator = new Cfo(period: 14); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } } diff --git a/Tests/test_updates_volatility.cs b/Tests/test_updates_volatility.cs index f8877a8e..ed2bedc0 100644 --- a/Tests/test_updates_volatility.cs +++ b/Tests/test_updates_volatility.cs @@ -101,4 +101,100 @@ public class VolatilityUpdateTests Assert.Equal(initialValue, finalValue, precision); } + + [Fact] + public void Tr_Update() + { + var indicator = new Tr(); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Ui_Update() + { + var indicator = new Ui(period: 14); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Vc_Update() + { + var indicator = new Vc(period: 20, deviations: 2.0); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Vov_Update() + { + var indicator = new Vov(period: 20); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Vr_Update() + { + var indicator = new Vr(shortPeriod: 10, longPeriod: 20); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Vs_Update() + { + var indicator = new Vs(period: 14, multiplier: 2.0); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } } diff --git a/Tests/test_updates_volume.cs b/Tests/test_updates_volume.cs index 472dd740..66058a26 100644 --- a/Tests/test_updates_volume.cs +++ b/Tests/test_updates_volume.cs @@ -156,4 +156,167 @@ public class VolumeUpdateTests Assert.Equal(initialValue, finalValue, precision); } + + [Fact] + public void Mfi_Update() + { + var indicator = new Mfi(period: 14); + TBar r = GetRandomBar(true); + + // Generate a sequence of bars for warmup + var warmupBars = new List(); + for (int i = 0; i < indicator.WarmupPeriod; i++) + { + var bar = GetRandomBar(IsNew: true); + warmupBars.Add(bar); + indicator.Calc(bar); + } + + // Calculate initial value after warmup + double initialValue = indicator.Calc(r); + + // Apply random updates + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + + // Reset and replay the same sequence + indicator.Init(); + foreach (var bar in warmupBars) + { + indicator.Calc(bar); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Nvi_Update() + { + var indicator = new Nvi(); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Obv_Update() + { + var indicator = new Obv(); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Pvi_Update() + { + var indicator = new Pvi(); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Pvol_Update() + { + var indicator = new Pvol(); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Pvo_Update() + { + var indicator = new Pvo(shortPeriod: 12, longPeriod: 26); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Pvr_Update() + { + var indicator = new Pvr(); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Pvt_Update() + { + var indicator = new Pvt(); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Tvi_Update() + { + var indicator = new Tvi(minTick: 0.5); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } } diff --git a/docs/indicators/indicators.md b/docs/indicators/indicators.md index d0d66932..6848ff29 100644 --- a/docs/indicators/indicators.md +++ b/docs/indicators/indicators.md @@ -1,183 +1,213 @@ # Indicators in QuanTAlib -⭐= Validation against several TA libraries
-✔️= Validation tests passed
-❌= Issue +\* = Returns multiple values -|**MOMENTUM INDICATORS**|**Class Name**|Skender.Stock|TALib.NETCore| -|--|:--:|:--:|:--:| -|DMI - Directional Movement Index|`?`|GetDmi|| -|DMX - Jurik Directional Movement Index|`?`||| -|MOM - Momentum|`?`||| -|VEL - Jurik Signal Velocity|`?`||| -|ADX - Average Directional Movement Index|`?`|GetAdx|Adx| -|ADXR - Average Directional Movement Index|`?`|Rating|Adxr| -|APO - Absolute Price Oscillator|`?`|Apo|| -|DPO - Detrended Price Oscillator|`?`|GetDpo|| -|MACD - Moving Average Convergence/Divergence|`?`||| -|PO - Price Oscillator|`?`||| -|PPO - Percentage Price Oscillator|`?`||| -|PMO - Price Momentum Oscillator|`?`|GetPmo|| -|PRS - Price Relative Strength|`?`|GetPrs|| -|ROC - Rate of Change|`?`|GetRoc|| -|TRIX - 1-day ROC of TEMA|`?`|GetTrix|| -|VORTEX - Vortex Indicator|`?`||| -
-|**VOLATILITY INDICATORS**|**Class Name**|Skender.Stock|TALib.NETCore| -|ADR - Average Daily Range|`?`||| -|ANDREW - Andrew's Pitchfork|`?`||| -|ATR - Average True Range|`Atr`|GetAtr|Atr| -|ATRP - Average True Range Percent|`?`||| -|ATRSTOP - ATR Trailing Stop|`?`|GetAtrStop|| -|BBANDS - Bollinger Bands®|`?`|BollingerBands|| -|CHAND - Chandelier Exit|`?`|GetChandelier|| -|CVI - Chaikins Volatility|`?`||| -|DON - Donchian Channels|`?`|GetDonchian|| -|FCB - Fractal Chaos Bands|`?`|GetFcb|| -|HV - Historical Volatility|`Hv`||| -|ICH - Ichimoku Cloud|`?`|GetIchimoku|| -|KEL - Keltner Channels|`?`|GetKeltner|| -|NATR - Normalized Average True Range|`?`|GetAtr|| -|CHN - Price Channel Indicator|`?`||| -|SAR - Parabolic Stop and Reverse|`?`|GetParabolicSar|| -|STARC - Starc Bands|`?`|GetStarcBands|| -|TR - True Range|`?`||| -|UI - Ulcer Index|`?`|GetUlcerIndex|| -|VSTOP - Volatility Stop|`?`|GetVolatilityStop|| -
-|**OSCILLATORS**|**Class Name**|Skender.Stock|TALib.NETCore| -|RSI - Relative Strength Index|`Rsi`|GetRsi|| -|RSX - Jurik Trend Strength Index|`Rsx`||| -|AC - Acceleration Oscillator|`?`||| -|AO - Awesome Oscillator|`?`|GetAwesome|| -|AROON - Aroon oscillator|`?`|GetAroon|Aroon| -|BOP - Balance of Power|`?`|GetBop|Bop| -|CCI - Commodity Channel Index|`?`|GetCci|Cci| -|CFO - Chande Forcast Oscillator|`?`||| -|CMO - Chande Momentum Oscillator|`Cmo`|GetCmo|Cmo| -|CHOP - Choppiness Index|`?`|GetChop|| -|COG - Ehler's Center of Gravity|`?`||| -|COPPOCK - Coppock Curve|`?`||| -|CRSI - Connor RSI|`?`|GetConnorsRsi|| -|CTI - Ehler's Correlation Trend Indicator|`?`||| -|DOSC - Derivative Oscillator|`?`||| -|EFI - Elder Ray's Force Index|`?`|GetElderRay|| -|FISHER - Fisher Transform|`?`||| -|FOSC - Forecast Oscillator|`?`||| -|GATOR - Williams Alliator Oscillator|`?`|GetGator|| -|KDJ - KDJ Indicator (trend reversal)|`?`||| -|KRI - Kairi Relative Index|`?`||| -|RVGI - Relative Vigor Index|`?`||| -|SMI - Stochastic Momentum Index|`?`|GetSmi|| -|SRSI - Stochastic RSI|`?`|GetStochRsi|| -|STC - Schaff Trend Cycle|`?`|GetStc|| -|STOCH - Stochastic Oscillator|`?`|GetStoch|| -|TSI - True Strength Index|`?`|GetTsi|| -|UO - Ultimate Oscillator|`?`|GetUltimate|| -|WILLR - Larry Williams' %R|`?`|GetWilliamsR|| -
-|**VOLUME INDICATORS**|**Class Name**|Skender.Stock|TALib.NETCore| -|ADL - Chaikin Accumulation Distribution Line|`?`|GetAdl|Ad| -|ADOSC - Chaikin Accumulation Distribution Oscillator|`?`|GetChaikinOsc|AdOsc| -|AOBV - Archer On-Balance Volume|`?`||| -|CMF - Chaikin Money Flow|`?`|GetCmf|| -|EOM - Ease of Movement|`?`||| -|KVO - Klinger Volume Oscillator|`?`|GetKvo|| -|MFI - Money Flow Index|`?`|GetMfi|| -|NVI - Negative Volume Index|`?`||| -|OBV - On-Balance Volume|`?`|GetObv|| -|PVI - Positive Volume Index|`?`||| -|PVOL - Price-Volume|`?`||| -|PVO - Percentage Volume Oscillator|`?`|GetPvo|| -|PVR - Price Volume Rank|`?`||| -|PVT - Price Volume Trend|`?`||| -|TVI - Trade Volume Index|`?`||| -|VP - Volume Profile|`?`||| -|VWAP - Volume Weighted Average Price|`?`|GetVwap|| -|VWMA - Volume Weighted Moving Average|`?`|GetVwma|| -
-|**NUMERICAL ANALYSIS**|**Class Name**|Skender.Stock|TALib.NETCore| -|BETA - Beta coefficient|`?`||| -|CORR - Correlation Coefficient|`?`||| -|CURVATURE - Rate of Change in Direction or Slope|`Curvature`||| -|ENTROPY - Measure of Uncertainty or Disorder|`Entropy`||| -|KURTOSIS - Measure of Tails/Peakedness|`Kurtosis`||| -|HUBER - Huber Loss|`Huber`||| -|HURST - Hurst Exponent|`?`|GetHurst|| -|MAX - Maximum with exponential decay|`Max`||| -|MEDIAN - Middle value|`Median`||| -|MIN - Minimum with exponential decay|`Min`||| -|MODE - Most Frequent Value|`Mode`||| -|PERCENTILE - Rank Order|`Percentile`||| -|RSQUARED - Coefficient of Determination R-Squared|`?`||| -|SKEW - Skewness, asymmetry of distribution|`Skew`||| -|SLOPE - Rate of Change, Linear Regression|`Slope`||| -|STDDEV - Standard Deviation, Measure of Spread|`Stddev`||| -|THEIL - Theil's U Statistics|`?`||| -|TSF - Time Series Forecast|`?`|✔️|✔️| -|VARIANCE - Average of Squared Deviations|`Variance`||| -|ZSCORE - Standardized Score|`Zscore`||| -
-|**ERRORS**|**Class Name**|Skender.Stock|TALib.NETCore| -|MAE - Mean Absolute Error|`Mae`||| -|MAPD - Mean Absolute Percentage Deviation|`Mapd`||| -|MAPE - Mean Absolute Percentage Error|`Mape`||| -|MASE - Mean Absolute Scaled Error|`Mase`||| -|MDA - Mean Directional Accuracy|`Mda`||| -|ME - Mean Error|`Me`||| -|MPE - Mean Percentage Error|`Mpe`||| -|MSE - Mean Squared Error|`Mse`||| -|MSLE - Mean Squared Logarithmic Error|`Msle`||| -|RAE - Relative Absolute Error|`Rae`||| -|RMSE - Root Mean Squared Error|`Rmse`||| -|RSE - Relative Squared Error|`Rse`||| -|RMSLE - Root Mean Squared Logarithmic Error|`Rmsle`||| -|SMAPE - Symmetric Mean Absolute Percentage Error|`Smape`||| -
-|**AVERAGES & TRENDS**|**Class Name**|Skender.Stock|TALib.NETCore| -|AFIRMA - Autoregressive Finite Impulse Response Moving Average|`Afirma`||| -|ALMA - Arnaud Legoux Moving Average|`Alma`|✔️|| -|DEMA - Double EMA Average|`Dema`|✔️|✔️| -|DSMA - Deviation Scaled Moving Average|`Dsma`||| -|DWMA - Double WMA Average|`Dwma`||| -|EMA - Exponential Moving Average|`Ema`|⭐|⭐| -|EPMA - Endpoint Moving Average|`Epma`|✔️|| -|FRAMA - Fractal Adaptive Moving Average|`Frama`||| -|FWMA - Fibonacci Weighted Moving Average|`Fwma`||| -|HILO - Gann High-Low Activator|`?`||| -|HTIT - Hilbert Transform Instantaneous Trendline|`Htit`|✔️|✔️| -|GMA - Gaussian-Weighted Moving Average|`Gma`||| -|HMA - Hull Moving Average|`Hma`|✔️|✔️| -|HWMA - Holt-Winter Moving Average|`Hwma`||| -|JMA - Jurik Moving Average|`Jma`||| -|JORDAN - Jordan Moving Average|`?`||| -|KAMA - Kaufman's Adaptive Moving Average|`Kama`|✔️|✔️| -|LTMA - Laguerre Transform Moving Average|`Ltma`||| -|MAAF - Median-Average Adaptive Filter|`Maaf`||| -|MAMA - MESA Adaptive Moving Average|`Mama`|✔️|✔️| -|MGDI - McGinley Dynamic Indicator|`Mgdi`|✔️|| -|MLMA - Minimal Lag Moving Average|`?`||| -|MMA - Modified Moving Average|`Mma`||| -|PPMA - Pivot Point Moving Average|`?`||| -|PWMA - Pascal's Weighted Moving Average|`Pwma`||| -|QEMA - Quad Exponential Moving Average|`Qema`||| -|RMA - WildeR's Moving Average|`Rma`||| -|SINEMA - Sine Weighted Moving Average|`Sinema`||| -|SMA - Simple Moving Average|`Sma`||| -|SMMA - Smoothed Moving Average|`Smma`|✔️|| -|SSF - Ehler's Super Smoother Filter|`?`||| -|SUPERTREND - Supertrend|`?`|✔️|| -|T3 - Tillson T3 Moving Average|`T3`|✔️|✔️| -|TEMA - Triple EMA Average|`Tema`|✔️|✔️| -|TRIMA - Triangular Moving Average|`Trima`|✔️|| -|VIDYA - Variable Index Dynamic Average|`Vidya`||| -|WMA - Weighted Moving Average|`Wma`|✔️|| -|ZLEMA - Zero Lag EMA Average|`Zlema`||| -
-|**BASIC TRANSFORMS**|**Class Name**|Skender.Stock|TALib.NETCore| -|OC2 - Midpoint price|`.OC2`|CandlePart.OC2|MidPoint| -|HL2 - Median Price|`.HL2`|CandlePart.HL2|MedPrice| -|HLC3 - Typical Price|`.HLC3`|CandlePart.HLC3|TypPrice| -|OHL3 - Mean Price|`.OHL3`|CandlePart.OHL3|| -|OHLC4 - Average Price|`.OHLC4`|CandlePart.OHLC4|AvgPrice| -|HLCC4 - Weighted Price|`.HLCC4`||WclPrice| +**Implementation Status:** +- Basic Transforms: 6 of 6 complete +- Averages & Trends: 33 of 33 complete +- Momentum: 16 of 17 complete +- Oscillators: 6 of 29 complete +- Volatility: 11 of 35 complete +- Volume: 15 of 19 complete +- Numerical Analysis: 13 of 20 complete +- Errors: 16 of 16 complete +- **Total: 116 of 175 indicators implemented (66%)** + +|**BASIC TRANSFORMS**|**Class Name**| +|--|:--:| +|OC2 - Midpoint price|`.OC2`| +|HL2 - Median Price|`.HL2`| +|HLC3 - Typical Price|`.HLC3`| +|OHL3 - Mean Price|`.OHL3`| +|OHLC4 - Average Price|`.OHLC4`| +|HLCC4 - Weighted Price|`.HLCC4`| + +|**AVERAGES & TRENDS**|**Class Name**| +|--|:--:| +|AFIRMA - Adaptive FIR Moving Average|`Afirma`| +|ALMA - Arnaud Legoux Moving Average|`Alma`| +|DEMA - Double Exponential Moving Average|`Dema`| +|DSMA - Dynamic Simple Moving Average|`Dsma`| +|DWMA - Dynamic Weighted Moving Average|`Dwma`| +|EMA - Exponential Moving Average|`Ema`| +|EPMA - Endpoint Moving Average|`Epma`| +|FRAMA - Fractal Adaptive Moving Average|`Frama`| +|FWMA - Forward Weighted Moving Average|`Fwma`| +|GMA - Gaussian Moving Average|`Gma`| +|HMA - Hull Moving Average|`Hma`| +|HTIT - Hilbert Transform Instantaneous Trendline|`Htit`| +|HWMA - Hann Weighted Moving Average|`Hwma`| +|JMA - Jurik Moving Average|`Jma`| +|KAMA - Kaufman Adaptive Moving Average|`Kama`| +|LTMA - Linear Time Moving Average|`Ltma`| +|MAAF - Moving Average Adaptive Filter|`Maaf`| +|MAMA* - MESA Adaptive Moving Average (MAMA, FAMA)|`Mama`| +|MGDI - McGinley Dynamic Indicator|`Mgdi`| +|MMA - Modified Moving Average|`Mma`| +|PWMA - Parabolic Weighted Moving Average|`Pwma`| +|QEMA - Quick Exponential Moving Average|`Qema`| +|REMA - Regularized Exponential Moving Average|`Rema`| +|RMA - Running Moving Average|`Rma`| +|SINEMA - Sine-weighted Moving Average|`Sinema`| +|SMA - Simple Moving Average|`Sma`| +|SMMA - Smoothed Moving Average|`Smma`| +|T3 - Triple Exponential Moving Average (T3)|`T3`| +|TEMA - Triple Exponential Moving Average|`Tema`| +|TRIMA - Triangular Moving Average|`Trima`| +|VIDYA - Variable Index Dynamic Average|`Vidya`| +|WMA - Weighted Moving Average|`Wma`| +|ZLEMA - Zero-Lag Exponential Moving Average|`Zlema`| + +|**MOMENTUM INDICATORS**|**Class Name**| +|--|:--:| +|ADX - Average Directional Movement Index|`Adx`| +|ADXR - Average Directional Movement Index Rating|`Adxr`| +|APO - Absolute Price Oscillator|`Apo`| +|DMI* - Directional Movement Index (DI+, DI-)|`Dmi`| +|DMX - Jurik Directional Movement Index|`Dmx`| +|DPO - Detrended Price Oscillator|`Dpo`| +|🚧 MACD* - Moving Average Convergence/Divergence|`Macd`| +|MOM - Momentum|`Mom`| +|PMO - Price Momentum Oscillator|`Pmo`| +|PO - Price Oscillator|`Po`| +|PPO - Percentage Price Oscillator|`Ppo`| +|PRS - Price Relative Strength|`Prs`| +|ROC - Rate of Change|`Roc`| +|TSI - True Strength Index|`Tsi`| +|TRIX - 1-day ROC of TEMA|`Trix`| +|VEL - Jurik Signal Velocity|`Vel`| +|VORTEX* - Vortex Indicator (VI+, VI-)|`Vortex`| + +|**OSCILLATORS**|**Class Name**| +|--|:--:| +|AC - Acceleration Oscillator|`Ac`| +|AO - Awesome Oscillator|`Ao`| +|AROON* - Aroon oscillator (Up, Down)|`Aroon`| +|🚧 BOP - Balance of Power|`Bop`| +|🚧 CCI - Commodity Channel Index|`Cci`| +|🚧 CFO - Chande Forcast Oscillator|`Cfo`| +|CMO - Chande Momentum Oscillator|`Cmo`| +|🚧 CHOP - Choppiness Index|`Chop`| +|🚧 COG - Ehler's Center of Gravity|`Cog`| +|🚧 COPPOCK - Coppock Curve|`Coppock`| +|🚧 CRSI - Connor RSI|`Crsi`| +|🚧 CTI - Ehler's Correlation Trend Indicator|`Cti`| +|🚧 DOSC - Derivative Oscillator|`Dosc`| +|🚧 EFI - Elder Ray's Force Index|`Efi`| +|🚧 FISHER - Fisher Transform|`Fisher`| +|🚧 FOSC - Forecast Oscillator|`Fosc`| +|🚧 GATOR* - Williams Alliator Oscillator (Upper Jaw, Lower Jaw, Teeth)|`Gator`| +|🚧 KDJ* - KDJ Indicator (K, D, J lines)|`Kdj`| +|🚧 KRI - Kairi Relative Index|`Kri`| +|RSI - Relative Strength Index|`Rsi`| +|RSX - Jurik Trend Strength Index|`Rsx`| +|🚧 RVGI* - Relative Vigor Index (RVGI, Signal)|`Rvgi`| +|🚧 SMI - Stochastic Momentum Index|`Smi`| +|🚧 SRSI* - Stochastic RSI (SRSI, Signal)|`Srsi`| +|🚧 STC - Schaff Trend Cycle|`Stc`| +|🚧 STOCH* - Stochastic Oscillator (%K, %D)|`Stoch`| +|🚧 TSI - True Strength Index|`Tsi`| +|🚧 UO - Ultimate Oscillator|`Uo`| +|🚧 WILLR - Larry Williams' %R|`Willr`| + +|**VOLATILITY INDICATORS**|**Class Name**| +|--|:--:| +|🚧 ADR - Average Daily Range|`Adr`| +|🚧 AP - Andrew's Pitchfork|`Ap`| +|ATR - Average True Range|`Atr`| +|🚧 ATRP - Average True Range Percent|`Atrp`| +|🚧 ATRS - ATR Trailing Stop|`Atrs`| +|🚧 BB* - Bollinger Bands® (Upper, Middle, Lower)|`Bb`| +|🚧 CCV - Close-to-Close Volatility|`Ccv`| +|🚧 CE - Chandelier Exit|`Ce`| +|🚧 CV - Conditional Volatility (ARCH/GARCH)|`Cv`| +|🚧 CVI - Chaikin's Volatility|`Cvi`| +|🚧 DC* - Donchian Channels (Upper, Middle, Lower)|`Dc`| +|🚧 EWMA - Exponential Weighted Moving Average Volatility|`Ewma`| +|🚧 FCB - Fractal Chaos Bands|`Fcb`| +|🚧 GKV - Garman-Klass Volatility|`Gkv`| +|🚧 HLV - High-Low Volatility|`Hlv`| +|HV - Historical Volatility|`Hv`| +|🚧 ICH* - Ichimoku Cloud (Conversion, Base, Leading Span A, Leading Span B, Lagging Span)|`Ich`| +|JVOLTY - Jurik Volatility|`Jvolty`| +|🚧 KC* - Keltner Channels (Upper, Middle, Lower)|`Kc`| +|🚧 NATR - Normalized Average True Range|`Natr`| +|🚧 PCH - Price Channel Indicator|`Pch`| +|🚧 PSAR* - Parabolic Stop and Reverse (Value, Trend)|`Psar`| +|🚧 PV - Parkinson Volatility|`Pv`| +|🚧 RSV - Rogers-Satchell Volatility|`Rsv`| +|RV - Realized Volatility|`Rv`| +|RVI - Relative Volatility Index|`Rvi`| +|🚧 STARC* - Starc Bands (Upper, Middle, Lower)|`Starc`| +|🚧 SV - Stochastic Volatility|`Sv`| +|TR - True Range|`Tr`| +|UI - Ulcer Index|`Ui`| +|VC* - Volatility Cone (Mean, Upper Bound, Lower Bound)|`Vc`| +|VOV - Volatility of Volatility|`Vov`| +|VR - Volatility Ratio|`Vr`| +|VS* - Volatility Stop (Long Stop, Short Stop)|`Vs`| +|🚧 YZV - Yang-Zhang Volatility|`Yzv`| + +|**VOLUME INDICATORS**|**Class Name**| +|--|:--:| +|ADL - Chaikin Accumulation Distribution Line|`Adl`| +|ADOSC - Chaikin Accumulation Distribution Oscillator|`Adosc`| +|AOBV - Archer On-Balance Volume|`Aobv`| +|CMF - Chaikin Money Flow|`Cmf`| +|EOM - Ease of Movement|`Eom`| +|KVO - Klinger Volume Oscillator|`Kvo`| +|MFI - Money Flow Index|`Mfi`| +|NVI - Negative Volume Index|`Nvi`| +|OBV - On-Balance Volume|`Obv`| +|PVI - Positive Volume Index|`Pvi`| +|PVOL - Price-Volume|`Pvol`| +|PVO - Percentage Volume Oscillator|`Pvo`| +|PVR - Price Volume Rank|`Pvr`| +|PVT - Price Volume Trend|`Pvt`| +|TVI - Trade Volume Index|`Tvi`| +|🚧 VF - Volume Force|`Vf`| +|🚧 VP - Volume Profile|`Vp`| +|🚧 VWAP - Volume Weighted Average Price|`Vwap`| +|🚧 VWMA - Volume Weighted Moving Average|`Vwma`| + +|**NUMERICAL ANALYSIS**|**Class Name**| +|--|:--:| +|🚧 BETA* - Beta coefficient (Beta, R-squared)|`Beta`| +|🚧 CORR* - Correlation Coefficient (Correlation, P-value)|`Corr`| +|CURVATURE - Rate of Change in Direction or Slope|`Curvature`| +|ENTROPY - Measure of Uncertainty or Disorder|`Entropy`| +|🚧 HUBER - Huber Loss|`Huber`| +|🚧 HURST - Hurst Exponent|`Hurst`| +|KURTOSIS - Measure of Tails/Peakedness|`Kurtosis`| +|MAX - Maximum with exponential decay|`Max`| +|MEDIAN - Middle value|`Median`| +|MIN - Minimum with exponential decay|`Min`| +|MODE - Most Frequent Value|`Mode`| +|PERCENTILE - Rank Order|`Percentile`| +|🚧 RSQUARED* - Coefficient of Determination (R-squared, Adjusted R-squared)|`Rsquared`| +|SKEW - Skewness, asymmetry of distribution|`Skew`| +|SLOPE - Rate of Change, Linear Regression|`Slope`| +|STDDEV - Standard Deviation, Measure of Spread|`Stddev`| +|🚧 THEIL* - Theil's U Statistics (U1, U2)|`Theil`| +|🚧 TSF* - Time Series Forecast (Forecast, Confidence Interval)|`Tsf`| +|VARIANCE - Average of Squared Deviations|`Variance`| +|ZSCORE - Standardized Score|`Zscore`| + +|**ERRORS**|**Class Name**| +|--|:--:| +|HUBER - Huber Loss|`Huber`| +|MAE - Mean Absolute Error|`Mae`| +|MAPD - Mean Absolute Percentage Deviation|`Mapd`| +|MAPE - Mean Absolute Percentage Error|`Mape`| +|MASE - Mean Absolute Scaled Error|`Mase`| +|MDA - Mean Directional Accuracy|`Mda`| +|ME - Mean Error|`Me`| +|MPE - Mean Percentage Error|`Mpe`| +|MSE - Mean Squared Error|`Mse`| +|MSLE - Mean Squared Logarithmic Error|`Msle`| +|RAE - Relative Absolute Error|`Rae`| +|RMSE - Root Mean Squared Error|`Rmse`| +|RMSLE - Root Mean Squared Logarithmic Error|`Rmsle`| +|RSE - Relative Squared Error|`Rse`| +|RSQUARED - R-Squared (Coefficient of Determination)|`Rsquared`| +|SMAPE - Symmetric Mean Absolute Percentage Error|`Smape`| diff --git a/lib/averages/_list.md b/lib/averages/_list.md index 7ad3da91..4273225c 100644 --- a/lib/averages/_list.md +++ b/lib/averages/_list.md @@ -1,3 +1,5 @@ +# Averages indicators + ✔️ AFIRMA - Adaptive FIR Moving Average ✔️ ALMA - Arnaud Legoux Moving Average ✔️ DEMA - Double Exponential Moving Average @@ -15,7 +17,7 @@ ✔️ KAMA - Kaufman Adaptive Moving Average ✔️ LTMA - Linear Time Moving Average ✔️ MAAF - Moving Average Adaptive Filter -✔️ MAMA - MESA Adaptive Moving Average +✔️ *MAMA - MESA Adaptive Moving Average (MAMA, FAMA) ✔️ MGDI - McGinley Dynamic Indicator ✔️ MMA - Modified Moving Average ✔️ PWMA - Parabolic Weighted Moving Average diff --git a/lib/momentum/Dpo.cs b/lib/momentum/Dpo.cs new file mode 100644 index 00000000..f88ad083 --- /dev/null +++ b/lib/momentum/Dpo.cs @@ -0,0 +1,112 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// DPO: Detrended Price Oscillator +/// A momentum indicator that removes the trend from price by comparing the current price +/// to a past moving average, helping to identify cycles in the price. +/// +/// +/// The DPO calculation process: +/// 1. Calculate the period shifted back by (period / 2 + 1) days +/// 2. Calculate SMA for the shifted period +/// 3. DPO = Price - SMA(Price, period) shifted back +/// +/// Key characteristics: +/// - Removes long-term trends +/// - Helps identify cycles +/// - Oscillates above and below zero +/// - Default period is 20 days +/// - Uses price displacement +/// +/// Formula: +/// DPO = Price - SMA(Price, period) shifted (period/2 + 1) bars back +/// +/// Market Applications: +/// - Cycle identification +/// - Overbought/Oversold conditions +/// - Price momentum +/// - Trading signals +/// - Market timing +/// +/// Sources: +/// Donald Dorsey - Original development +/// https://www.investopedia.com/terms/d/detrended-price-oscillator-dpo.asp +/// +/// Note: DPO helps identify cycles by removing the trend component from the price data +/// + +[SkipLocalsInit] +public sealed class Dpo : AbstractBase +{ + private readonly int _shift; + private readonly CircularBuffer _prices; + private readonly CircularBuffer _sma; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Dpo(int period = 20) + { + _shift = period / 2 + 1; + WarmupPeriod = period + _shift; + Name = $"DPO({period})"; + _prices = new CircularBuffer(WarmupPeriod); + _sma = new CircularBuffer(period); + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Dpo(object source, int period = 20) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prices.Clear(); + _sma.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Add current price to buffer + _prices.Add(BarInput.Close); + + // Need enough prices for the shifted SMA calculation + if (_index <= _shift) + { + return 0; + } + + // Add price from shift periods ago to SMA buffer + _sma.Add(_prices[_shift]); + + // Need enough prices for full calculation + if (_index <= WarmupPeriod) + { + return 0; + } + + // Calculate DPO + double dpo = BarInput.Close - _sma.Average(); + + IsHot = _index >= WarmupPeriod; + return dpo; + } +} diff --git a/lib/momentum/Tsi.cs b/lib/momentum/Tsi.cs new file mode 100644 index 00000000..350318c7 --- /dev/null +++ b/lib/momentum/Tsi.cs @@ -0,0 +1,151 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// TSI: True Strength Index +/// A momentum indicator that shows both trend direction and overbought/oversold conditions +/// by using two smoothing steps on price changes. +/// +/// +/// The TSI calculation process: +/// 1. Calculate price change (PC): +/// PC = Close - Previous Close +/// 2. Calculate absolute price change (APC): +/// APC = |PC| +/// 3. Double smooth both PC and APC using EMA: +/// First PC EMA = EMA(PC, firstPeriod) +/// Second PC EMA = EMA(First PC EMA, secondPeriod) +/// First APC EMA = EMA(APC, firstPeriod) +/// Second APC EMA = EMA(First APC EMA, secondPeriod) +/// 4. Calculate TSI: +/// TSI = (Second PC EMA / Second APC EMA) * 100 +/// +/// Key characteristics: +/// - Double smoothed momentum indicator +/// - Oscillates between +100 and -100 +/// - Default periods are 25 and 13 +/// - Shows trend direction +/// - Identifies overbought/oversold +/// +/// Formula: +/// TSI = (EMA(EMA(PC, r), s) / EMA(EMA(|PC|, r), s)) * 100 +/// where: +/// PC = Close - Previous Close +/// r = first period (default 25) +/// s = second period (default 13) +/// +/// Market Applications: +/// - Trend direction +/// - Overbought/Oversold levels +/// - Centerline crossovers +/// - Divergence analysis +/// - Signal line crossovers +/// +/// Sources: +/// William Blau - Original development (1991) +/// https://www.investopedia.com/terms/t/tsi.asp +/// +/// Note: Values above +25 indicate overbought conditions, while values below -25 indicate oversold conditions +/// + +[SkipLocalsInit] +public sealed class Tsi : AbstractBase +{ + private readonly int _firstPeriod; + private double _prevClose; + private double _pcFirstEma; + private double _pcSecondEma; + private double _apcFirstEma; + private double _apcSecondEma; + private readonly double _firstAlpha; + private readonly double _secondAlpha; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Tsi(int firstPeriod = 25, int secondPeriod = 13) + { + _firstPeriod = firstPeriod; + WarmupPeriod = firstPeriod + secondPeriod; + Name = $"TSI({_firstPeriod},{secondPeriod})"; + _firstAlpha = 2.0 / (firstPeriod + 1); + _secondAlpha = 2.0 / (secondPeriod + 1); + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Tsi(object source, int firstPeriod = 25, int secondPeriod = 13) : this(firstPeriod, secondPeriod) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prevClose = 0; + _pcFirstEma = 0; + _pcSecondEma = 0; + _apcFirstEma = 0; + _apcSecondEma = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Skip first period to establish previous close + if (_index == 1) + { + _prevClose = BarInput.Close; + return 0; + } + + // Calculate price changes + double pc = BarInput.Close - _prevClose; + double apc = Math.Abs(pc); + + // Initialize or update EMAs + if (_index <= _firstPeriod) + { + _pcFirstEma = pc; + _apcFirstEma = apc; + } + else + { + _pcFirstEma = (_firstAlpha * pc) + ((1 - _firstAlpha) * _pcFirstEma); + _apcFirstEma = (_firstAlpha * apc) + ((1 - _firstAlpha) * _apcFirstEma); + } + + if (_index <= WarmupPeriod) + { + _pcSecondEma = _pcFirstEma; + _apcSecondEma = _apcFirstEma; + } + else + { + _pcSecondEma = (_secondAlpha * _pcFirstEma) + ((1 - _secondAlpha) * _pcSecondEma); + _apcSecondEma = (_secondAlpha * _apcFirstEma) + ((1 - _secondAlpha) * _apcSecondEma); + } + + // Store current close for next calculation + _prevClose = BarInput.Close; + + // Calculate TSI + double tsi = Math.Abs(_apcSecondEma) > double.Epsilon ? (_pcSecondEma / _apcSecondEma) * 100 : 0; + + IsHot = _index >= WarmupPeriod; + return tsi; + } +} diff --git a/lib/momentum/Vortex.cs b/lib/momentum/Vortex.cs new file mode 100644 index 00000000..f113f193 --- /dev/null +++ b/lib/momentum/Vortex.cs @@ -0,0 +1,162 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// VORTEX: Vortex Indicator +/// A technical indicator consisting of two oscillating lines that identify trend reversals +/// and confirm current trends based on the highs and lows of the previous period. +/// +/// +/// The Vortex calculation process: +/// 1. Calculate True Range (TR): +/// TR = max(High - Low, |High - Previous Close|, |Low - Previous Close|) +/// 2. Calculate +VM (Positive Movement): +/// +VM = |Current High - Previous Low| +/// 3. Calculate -VM (Negative Movement): +/// -VM = |Current Low - Previous High| +/// 4. Calculate period sums: +/// TR Period Sum = Sum(TR, period) +/// +VM Period Sum = Sum(+VM, period) +/// -VM Period Sum = Sum(-VM, period) +/// 5. Calculate +VI and -VI: +/// +VI = +VM Period Sum / TR Period Sum +/// -VI = -VM Period Sum / TR Period Sum +/// +/// Key characteristics: +/// - Two oscillating lines (+VI and -VI) +/// - No upper or lower bounds +/// - Default period is 14 days +/// - Crossovers signal trend changes +/// - Uses true range normalization +/// +/// Formula: +/// +VI = Sum(+VM, period) / Sum(TR, period) +/// -VI = Sum(-VM, period) / Sum(TR, period) +/// +/// Market Applications: +/// - Trend identification +/// - Trend reversals +/// - Trend confirmation +/// - Trading signals +/// - Market momentum +/// +/// Sources: +/// Etienne Botes and Douglas Siepman - Original development (2010) +/// https://www.investopedia.com/terms/v/vortex-indicator-vi.asp +/// +/// Note: When +VI crosses above -VI, it signals a potential uptrend, and vice versa +/// + +[SkipLocalsInit] +public sealed class Vortex : AbstractBase +{ + private readonly CircularBuffer _tr; + private readonly CircularBuffer _vmPlus; + private readonly CircularBuffer _vmMinus; + private double _prevHigh; + private double _prevLow; + private double _prevClose; + public double _viPlus { get; set; } + public double _viMinus { get; set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vortex(int period = 14) + { + WarmupPeriod = period + 1; // Need one extra period for previous values + Name = $"VORTEX({period})"; + _tr = new CircularBuffer(period); + _vmPlus = new CircularBuffer(period); + _vmMinus = new CircularBuffer(period); + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vortex(object source, int period = 14) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prevHigh = 0; + _prevLow = 0; + _prevClose = 0; + _viPlus = 0; + _viMinus = 0; + _tr.Clear(); + _vmPlus.Clear(); + _vmMinus.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Skip first period to establish previous values + if (_index == 1) + { + _prevHigh = BarInput.High; + _prevLow = BarInput.Low; + _prevClose = BarInput.Close; + return 0; + } + + // Calculate True Range + double tr = Math.Max(BarInput.High - BarInput.Low, + Math.Max(Math.Abs(BarInput.High - _prevClose), + Math.Abs(BarInput.Low - _prevClose))); + + // Calculate VM+ and VM- + double vmPlus = Math.Abs(BarInput.High - _prevLow); + double vmMinus = Math.Abs(BarInput.Low - _prevHigh); + + // Add values to buffers + _tr.Add(tr); + _vmPlus.Add(vmPlus); + _vmMinus.Add(vmMinus); + + // Calculate VI+ and VI- + double trSum = _tr.Sum(); + if (Math.Abs(trSum) > double.Epsilon) + { + _viPlus = _vmPlus.Sum() / trSum; + _viMinus = _vmMinus.Sum() / trSum; + } + + // Store current values for next calculation + _prevHigh = BarInput.High; + _prevLow = BarInput.Low; + _prevClose = BarInput.Close; + + // Return the difference between VI+ and VI- + double vortex = _viPlus - _viMinus; + + IsHot = _index >= WarmupPeriod; + return vortex; + } + + /// + /// Gets the positive Vortex line (VI+) + /// + public double ViPlus => _viPlus; + + /// + /// Gets the negative Vortex line (VI-) + /// + public double ViMinus => _viMinus; +} diff --git a/lib/momentum/_list.md b/lib/momentum/_list.md index fe0e04cc..e141b879 100644 --- a/lib/momentum/_list.md +++ b/lib/momentum/_list.md @@ -1,20 +1,20 @@ # Momentum indicators -Done: 12, Todo: 5 +Done: 15, Todo: 2 ✔️ ADX - Average Directional Movement Index ✔️ ADXR - Average Directional Movement Index Rating ✔️ APO - Absolute Price Oscillator -✔️ DMI - Directional Movement Index +✔️ *DMI - Directional Movement Index (DI+, DI-) ✔️ DMX - Jurik Directional Movement Index -DPO - Detrended Price Oscillator -MACD - Moving Average Convergence/Divergence +✔️ DPO - Detrended Price Oscillator +*MACD - Moving Average Convergence/Divergence (MACD, Signal, Histogram) ✔️ MOM - Momentum ✔️ PMO - Price Momentum Oscillator ✔️ PO - Price Oscillator ✔️ PPO - Percentage Price Oscillator ✔️ PRS - Price Relative Strength ✔️ ROC - Rate of Change -TSI - True Strength Index +✔️ TSI - True Strength Index ✔️ TRIX - 1-day ROC of TEMA ✔️ VEL - Jurik Signal Velocity -VORTEX - Vortex Indicator +✔️ *VORTEX - Vortex Indicator (VI+, VI-) diff --git a/lib/oscillators/Bop.cs b/lib/oscillators/Bop.cs new file mode 100644 index 00000000..210805ca --- /dev/null +++ b/lib/oscillators/Bop.cs @@ -0,0 +1,66 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// BOP: Balance of Power +/// A momentum oscillator that measures the strength of buying and selling pressure by comparing +/// closing prices to their corresponding opening prices. +/// +/// +/// The BOP calculation process: +/// 1. Calculate (Close - Open) / (High - Low) for each period +/// 2. A positive BOP indicates buying pressure (bullish) +/// 3. A negative BOP indicates selling pressure (bearish) +/// +/// Key characteristics: +/// - Oscillates above and below zero +/// - No upper or lower bounds +/// - Zero line acts as equilibrium between buying and selling pressure +/// - Can be used to identify potential trend reversals and divergences +/// +/// Formula: +/// BOP = (Close - Open) / (High - Low) +/// +/// Sources: +/// Igor Livshin (1990s) +/// https://www.investopedia.com/terms/b/bop.asp +/// + +[SkipLocalsInit] +public sealed class Bop : AbstractBase +{ + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Bop(object source) : this() + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Bop() + { + WarmupPeriod = 1; + Name = "BOP"; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + var range = BarInput.High - BarInput.Low; + if (range <= double.Epsilon) return 0; + + return (BarInput.Close - BarInput.Open) / range; + } +} diff --git a/lib/oscillators/Cci.cs b/lib/oscillators/Cci.cs new file mode 100644 index 00000000..56e1135f --- /dev/null +++ b/lib/oscillators/Cci.cs @@ -0,0 +1,101 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// CCI: Commodity Channel Index +/// A momentum oscillator used to identify cyclical trends and measure the deviation of price +/// from its statistical mean. +/// +/// +/// The CCI calculation process: +/// 1. Calculate Typical Price (TP) = (High + Low + Close) / 3 +/// 2. Calculate Simple Moving Average of TP +/// 3. Calculate Mean Deviation +/// 4. CCI = (TP - SMA(TP)) / (0.015 * Mean Deviation) +/// +/// Key characteristics: +/// - Oscillates above and below zero +/// - Typically ranges between +100 and -100 +/// - Values above +100 indicate overbought conditions +/// - Values below -100 indicate oversold conditions +/// - Can identify trend strength and reversals +/// +/// Formula: +/// CCI = (TypicalPrice - SMA(TypicalPrice, period)) / (0.015 * MeanDeviation) +/// where: +/// - TypicalPrice = (High + Low + Close) / 3 +/// - MeanDeviation = Mean(|TP - SMA(TP)|) +/// +/// Sources: +/// Donald Lambert (1980) +/// https://www.investopedia.com/terms/c/commoditychannelindex.asp +/// + +[SkipLocalsInit] +public sealed class Cci : AbstractBase +{ + private readonly int _period; + private readonly Sma _sma; + private readonly double[] _typicalPrices; + private readonly double _constant = 0.015; + + /// The data source object that publishes updates. + /// The calculation period (default: 20) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cci(object source, int period = 20) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cci(int period = 20) + { + _period = period; + _sma = new Sma(period); + _typicalPrices = new double[period]; + WarmupPeriod = period; + Name = "CCI"; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateMeanDeviation(double typicalPrice, double smaValue) + { + var sum = 0.0; + var count = System.Math.Min(_period, _index + 1); + + for (var i = 0; i < count; i++) + { + sum += System.Math.Abs(_typicalPrices[i] - smaValue); + } + + return sum / count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + var typicalPrice = (BarInput.High + BarInput.Low + BarInput.Close) / 3.0; + var idx = _index % _period; + _typicalPrices[idx] = typicalPrice; + + var smaValue = _sma.Calc(typicalPrice, BarInput.IsNew); + if (_index < _period - 1) return double.NaN; + + var meanDeviation = CalculateMeanDeviation(typicalPrice, smaValue); + if (meanDeviation <= double.Epsilon) return 0; + + return (typicalPrice - smaValue) / (_constant * meanDeviation); + } +} diff --git a/lib/oscillators/Cfo.cs b/lib/oscillators/Cfo.cs new file mode 100644 index 00000000..d2693a36 --- /dev/null +++ b/lib/oscillators/Cfo.cs @@ -0,0 +1,117 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// CFO: Chande Forecast Oscillator +/// A momentum oscillator that measures the percentage difference between the actual price +/// and its linear regression forecast value. +/// +/// +/// The CFO calculation process: +/// 1. Calculate linear regression forecast value for the current period +/// 2. Calculate percentage difference between actual price and forecast +/// +/// Key characteristics: +/// - Oscillates above and below zero +/// - Measures deviation of price from its forecasted value +/// - Positive values indicate price is above forecast (bullish) +/// - Negative values indicate price is below forecast (bearish) +/// - Can identify potential trend reversals and price divergences +/// +/// Formula: +/// CFO = ((Price - Forecast) / Price) * 100 +/// where: +/// - Price is typically the closing price +/// - Forecast is the linear regression forecast value +/// +/// Sources: +/// Tushar Chande (1990s) +/// Technical Analysis of Stocks and Commodities magazine +/// + +[SkipLocalsInit] +public sealed class Cfo : AbstractBase +{ + private readonly int _period; + private readonly double[] _prices; + private double _sumX; + private double _sumY; + private double _sumXY; + private double _sumX2; + + /// The data source object that publishes updates. + /// The calculation period (default: 14) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cfo(object source, int period = 14) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cfo(int period = 14) + { + _period = period; + _prices = new double[period]; + WarmupPeriod = period; + Name = "CFO"; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void UpdateSums(double oldPrice, double newPrice, int oldX, int newX) + { + _sumY -= oldPrice; + _sumY += newPrice; + _sumXY -= oldPrice * oldX; + _sumXY += newPrice * newX; + _sumX -= oldX; + _sumX += newX; + _sumX2 -= oldX * oldX; + _sumX2 += newX * newX; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateForecast() + { + var count = System.Math.Min(_period, _index + 1); + var n = (double)count; + + // Calculate linear regression coefficients + var slope = (n * _sumXY - _sumX * _sumY) / (n * _sumX2 - _sumX * _sumX); + var intercept = (_sumY - slope * _sumX) / n; + + // Calculate forecast for next period + return intercept + slope * count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + var price = Input.Value; + var idx = _index % _period; + var oldPrice = _prices[idx]; + _prices[idx] = price; + + var oldX = idx + 1; + var newX = _index < _period ? idx + 1 : _period; + + UpdateSums(oldPrice, price, oldX, newX); + if (_index < _period - 1) return double.NaN; + + var forecast = CalculateForecast(); + if (price <= double.Epsilon) return 0; + + return ((price - forecast) / price) * 100; + } +} diff --git a/lib/oscillators/_list.md b/lib/oscillators/_list.md index 99e19436..418edeba 100644 --- a/lib/oscillators/_list.md +++ b/lib/oscillators/_list.md @@ -3,7 +3,7 @@ Done: 6, Todo: 23 ✔️ AC - Acceleration Oscillator ✔️ AO - Awesome Oscillator -✔️ AROON - Aroon oscillator +✔️ *AROON - Aroon oscillator (Up, Down) BOP - Balance of Power CCI - Commodity Channel Index CFO - Chande Forcast Oscillator @@ -17,16 +17,16 @@ DOSC - Derivative Oscillator EFI - Elder Ray's Force Index FISHER - Fisher Transform FOSC - Forecast Oscillator -GATOR - Williams Alliator Oscillator -KDJ - KDJ Indicator (trend reversal) +*GATOR - Williams Alliator Oscillator (Upper Jaw, Lower Jaw, Teeth) +*KDJ - KDJ Indicator (K, D, J lines) KRI - Kairi Relative Index ✔️ RSI - Relative Strength Index ✔️ RSX - Jurik Trend Strength Index -RVGI - Relative Vigor Index +*RVGI - Relative Vigor Index (RVGI, Signal) SMI - Stochastic Momentum Index -SRSI - Stochastic RSI +*SRSI - Stochastic RSI (SRSI, Signal) STC - Schaff Trend Cycle -STOCH - Stochastic Oscillator +*STOCH - Stochastic Oscillator (%K, %D) TSI - True Strength Index UO - Ultimate Oscillator WILLR - Larry Williams' %R diff --git a/lib/patterns/_list.md b/lib/patterns/_list.md index 8b54056f..99a7836e 100644 --- a/lib/patterns/_list.md +++ b/lib/patterns/_list.md @@ -2,10 +2,10 @@ Done: 0, Todo: 8 DOJI - Doji Candlestick Pattern -ER - Elder Ray Pattern +*ER - Elder Ray Pattern (Bull Power, Bear Power) MARU - Marubozu Candlestick Pattern -PIV - Pivot Points -PP - Price Pivots -RPP - Rolling Pivot Points +*PIV - Pivot Points (Support 1-3, Pivot, Resistance 1-3) +*PP - Price Pivots (Support 1-3, Pivot, Resistance 1-3) +*RPP - Rolling Pivot Points (Support 1-3, Pivot, Resistance 1-3) WF - Williams Fractal ZZ - Zig Zag Pattern diff --git a/lib/statistics/_list.md b/lib/statistics/_list.md index 627de7c8..fa7932fd 100644 --- a/lib/statistics/_list.md +++ b/lib/statistics/_list.md @@ -1,8 +1,8 @@ # Statistics indicators Done: 13, Todo: 6 -BETA - Beta coefficient -CORR - Correlation Coefficient +*BETA - Beta coefficient (Beta, R-squared) +*CORR - Correlation Coefficient (Correlation, P-value) ✔️ CURVATURE - Rate of Change in Direction or Slope ✔️ ENTROPY - Measure of Uncertainty or Disorder HUBER - Huber Loss @@ -13,11 +13,11 @@ HURST - Hurst Exponent ✔️ MIN - Minimum with exponential decay ✔️ MODE - Most Frequent Value ✔️ PERCENTILE - Rank Order -RSQUARED - Coefficient of Determination R-Squared +*RSQUARED - Coefficient of Determination (R-squared, Adjusted R-squared) ✔️ SKEW - Skewness, asymmetry of distribution ✔️ SLOPE - Rate of Change, Linear Regression ✔️ STDDEV - Standard Deviation, Measure of Spread -THEIL - Theil's U Statistics -TSF - Time Series Forecast +*THEIL - Theil's U Statistics (U1, U2) +*TSF - Time Series Forecast (Forecast, Confidence Interval) ✔️ VARIANCE - Average of Squared Deviations ✔️ ZSCORE - Standardized Score diff --git a/lib/volatility/Tr.cs b/lib/volatility/Tr.cs new file mode 100644 index 00000000..6e60d80f --- /dev/null +++ b/lib/volatility/Tr.cs @@ -0,0 +1,102 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// TR: True Range +/// A basic volatility measure that represents the greatest of three price ranges: +/// current high-low, current high-previous close, or current low-previous close. +/// +/// +/// The TR calculation process: +/// 1. Calculate three differences: +/// - Current High minus Current Low +/// - |Current High minus Previous Close| +/// - |Current Low minus Previous Close| +/// 2. TR is the maximum of these three values +/// +/// Key characteristics: +/// - Basic volatility measure +/// - Accounts for gaps between trading periods +/// - Foundation for other indicators (ATR, etc.) +/// - No upper bound +/// - Always positive +/// +/// Formula: +/// TR = max(High - Low, |High - Previous Close|, |Low - Previous Close|) +/// +/// Market Applications: +/// - Volatility measurement +/// - Stop loss placement +/// - Position sizing +/// - Market analysis +/// - Risk assessment +/// +/// Sources: +/// J. Welles Wilder Jr. - Original development +/// https://www.investopedia.com/terms/t/truerange.asp +/// +/// Note: True Range accounts for gaps between periods, making it more accurate than simple high-low range +/// + +[SkipLocalsInit] +public sealed class Tr : AbstractBase +{ + private double _prevClose; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Tr() + { + WarmupPeriod = 2; // Need previous close + Name = "TR"; + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Tr(object source) : this() + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prevClose = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Skip first period to establish previous close + if (_index == 1) + { + _prevClose = BarInput.Close; + return BarInput.High - BarInput.Low; + } + + // Calculate True Range + double tr = Math.Max(BarInput.High - BarInput.Low, + Math.Max(Math.Abs(BarInput.High - _prevClose), + Math.Abs(BarInput.Low - _prevClose))); + + // Store current close for next calculation + _prevClose = BarInput.Close; + + IsHot = _index >= WarmupPeriod; + return tr; + } +} diff --git a/lib/volatility/Ui.cs b/lib/volatility/Ui.cs new file mode 100644 index 00000000..cfa5f96f --- /dev/null +++ b/lib/volatility/Ui.cs @@ -0,0 +1,114 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// UI: Ulcer Index +/// A technical indicator that measures downside risk by incorporating both +/// the depth and duration of price declines over a given period. +/// +/// +/// The UI calculation process: +/// 1. Calculate percentage drawdown from recent high for each period +/// 2. Square the drawdowns to emphasize larger declines +/// 3. Calculate the average of squared drawdowns +/// 4. Take the square root of the average +/// +/// Key characteristics: +/// - Measures downside volatility +/// - Emphasizes larger drawdowns +/// - Default period is 14 days +/// - Always positive +/// - No upper bound +/// +/// Formula: +/// Drawdown = ((Close - 14-period High) / 14-period High) * 100 +/// UI = sqrt(sum(Drawdown^2) / period) +/// +/// Market Applications: +/// - Risk assessment +/// - Portfolio analysis +/// - Trading system evaluation +/// - Market timing +/// - Trend strength measurement +/// +/// Sources: +/// Peter Martin - Original development (1987) +/// https://www.investopedia.com/terms/u/ulcerindex.asp +/// +/// Note: Higher values indicate higher risk due to deeper or more frequent drawdowns +/// + +[SkipLocalsInit] +public sealed class Ui : AbstractBase +{ + private readonly int _period; + private readonly CircularBuffer _prices; + private readonly CircularBuffer _drawdowns; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Ui(int period = 14) + { + _period = period; + WarmupPeriod = period; + Name = $"UI({_period})"; + _prices = new CircularBuffer(period); + _drawdowns = new CircularBuffer(period); + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Ui(object source, int period = 14) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prices.Clear(); + _drawdowns.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Add current price to buffer + _prices.Add(BarInput.Close); + + // Need enough prices for calculation + if (_index <= _period) + { + return 0; + } + + // Calculate maximum price in period + double maxPrice = _prices.Max(); + + // Calculate percentage drawdown + double drawdown = Math.Abs(maxPrice) > double.Epsilon ? ((BarInput.Close - maxPrice) / maxPrice) * 100 : 0; + + // Add squared drawdown to buffer + _drawdowns.Add(drawdown * drawdown); + + // Calculate Ulcer Index + double ui = Math.Sqrt(_drawdowns.Average()); + + IsHot = _index >= WarmupPeriod; + return ui; + } +} diff --git a/lib/volatility/Vc.cs b/lib/volatility/Vc.cs new file mode 100644 index 00000000..0303fc30 --- /dev/null +++ b/lib/volatility/Vc.cs @@ -0,0 +1,164 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// VC: Volatility Cone +/// A technical indicator that analyzes volatility across different time periods +/// to identify normal ranges and extreme values. +/// +/// +/// The VC calculation process: +/// 1. Calculate volatility for the specified period +/// 2. Track mean and standard deviation of volatility +/// 3. Calculate upper and lower bounds: +/// Upper = Mean + (deviations * StdDev) +/// Lower = Mean - (deviations * StdDev) +/// +/// Key characteristics: +/// - Multi-period volatility analysis +/// - Statistical approach +/// - Default period is 20 days +/// - Returns mean and bounds +/// - Adaptive to market conditions +/// +/// Formula: +/// Volatility = StdDev(Returns) * sqrt(252) // Annualized +/// Upper = Mean(Volatility) + (deviations * StdDev(Volatility)) +/// Lower = Mean(Volatility) - (deviations * StdDev(Volatility)) +/// +/// Market Applications: +/// - Options trading +/// - Risk assessment +/// - Volatility forecasting +/// - Trading strategy development +/// - Market regime analysis +/// +/// Sources: +/// https://www.investopedia.com/terms/v/volatility-cone.asp +/// +/// Note: Returns three values: mean volatility and its upper/lower bounds +/// + +[SkipLocalsInit] +public sealed class Vc : AbstractBase +{ + private readonly int _period; + private readonly double _deviations; + private readonly CircularBuffer _returns; + private readonly CircularBuffer _volatilities; + private double _prevClose; + private double _upperBound; + private double _lowerBound; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vc(int period = 20, double deviations = 2.0) + { + _period = period; + _deviations = deviations; + WarmupPeriod = period * 2; // Need enough data for stable statistics + Name = $"VC({_period},{_deviations})"; + _returns = new CircularBuffer(period); + _volatilities = new CircularBuffer(period); + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vc(object source, int period = 20, double deviations = 2.0) : this(period, deviations) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prevClose = 0; + _upperBound = 0; + _lowerBound = 0; + _returns.Clear(); + _volatilities.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private double CalculateVariance(CircularBuffer buffer) + { + if (buffer.Count == 0) return 0; + double mean = buffer.Average(); + double sumSquaredDiff = 0; + for (int i = 0; i < buffer.Count; i++) + { + double diff = buffer[i] - mean; + sumSquaredDiff += diff * diff; + } + return sumSquaredDiff / buffer.Count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Skip first period to establish previous close + if (_index == 1) + { + _prevClose = BarInput.Close; + return 0; + } + + // Calculate return + double ret = Math.Abs(_prevClose) > double.Epsilon ? Math.Log(BarInput.Close / _prevClose) : 0; + _returns.Add(ret); + + // Store current close for next calculation + _prevClose = BarInput.Close; + + // Need enough returns for volatility calculation + if (_index <= _period) + { + return 0; + } + + // Calculate current volatility (annualized) + double vol = Math.Sqrt(CalculateVariance(_returns)) * Math.Sqrt(252); + _volatilities.Add(vol); + + // Need enough volatilities for cone calculation + if (_index <= WarmupPeriod) + { + return vol; + } + + // Calculate mean and standard deviation of volatilities + double meanVol = _volatilities.Average(); + double stdVol = Math.Sqrt(CalculateVariance(_volatilities)); + + // Calculate bounds + _upperBound = meanVol + (_deviations * stdVol); + _lowerBound = Math.Max(0, meanVol - (_deviations * stdVol)); + + IsHot = _index >= WarmupPeriod; + return meanVol; + } + + /// + /// Gets the upper bound of the volatility cone + /// + public double UpperBound => _upperBound; + + /// + /// Gets the lower bound of the volatility cone + /// + public double LowerBound => _lowerBound; +} diff --git a/lib/volatility/Vov.cs b/lib/volatility/Vov.cs new file mode 100644 index 00000000..c840eea0 --- /dev/null +++ b/lib/volatility/Vov.cs @@ -0,0 +1,131 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// VOV: Volatility of Volatility +/// A technical indicator that measures the volatility of volatility itself, +/// providing insight into the stability of market volatility. +/// +/// +/// The VOV calculation process: +/// 1. Calculate primary volatility (e.g., using True Range) +/// 2. Calculate standard deviation of primary volatility +/// 3. Normalize result for comparison +/// +/// Key characteristics: +/// - Second-order volatility measure +/// - Default period is 20 days +/// - Always positive +/// - No upper bound +/// - Measures volatility stability +/// +/// Formula: +/// Primary Volatility = TR (True Range) +/// VOV = StdDev(Primary Volatility, period) / Average(Primary Volatility, period) +/// +/// Market Applications: +/// - Risk of risk assessment +/// - Volatility regime changes +/// - Market stability analysis +/// - Trading strategy adaptation +/// - Risk management +/// +/// Note: Higher values indicate more unstable volatility conditions +/// + +[SkipLocalsInit] +public sealed class Vov : AbstractBase +{ + private readonly int _period; + private readonly CircularBuffer _volatilities; + private double _prevClose; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vov(int period = 20) + { + _period = period; + WarmupPeriod = period + 1; // Need extra period for TR calculation + Name = $"VOV({_period})"; + _volatilities = new CircularBuffer(period); + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vov(object source, int period = 20) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prevClose = 0; + _volatilities.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private double CalculateVariance(CircularBuffer buffer) + { + if (buffer.Count == 0) return 0; + double mean = buffer.Average(); + double sumSquaredDiff = 0; + for (int i = 0; i < buffer.Count; i++) + { + double diff = buffer[i] - mean; + sumSquaredDiff += diff * diff; + } + return sumSquaredDiff / buffer.Count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Skip first period to establish previous close + if (_index == 1) + { + _prevClose = BarInput.Close; + return 0; + } + + // Calculate True Range as primary volatility measure + double tr = Math.Max(BarInput.High - BarInput.Low, + Math.Max(Math.Abs(BarInput.High - _prevClose), + Math.Abs(BarInput.Low - _prevClose))); + + // Store current close for next calculation + _prevClose = BarInput.Close; + + // Add volatility to buffer + _volatilities.Add(tr); + + // Need enough volatilities for VOV calculation + if (_index <= _period) + { + return 0; + } + + // Calculate mean volatility + double meanVol = _volatilities.Average(); + + // Calculate VOV (normalized standard deviation) + double vov = meanVol > double.Epsilon ? Math.Sqrt(CalculateVariance(_volatilities)) / meanVol : 0; + + IsHot = _index >= WarmupPeriod; + return vov; + } +} diff --git a/lib/volatility/Vr.cs b/lib/volatility/Vr.cs new file mode 100644 index 00000000..009d6e56 --- /dev/null +++ b/lib/volatility/Vr.cs @@ -0,0 +1,135 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// VR: Volatility Ratio +/// A technical indicator that compares volatility across different time periods +/// to identify changes in market conditions. +/// +/// +/// The VR calculation process: +/// 1. Calculate short-term volatility +/// 2. Calculate long-term volatility +/// 3. Calculate ratio between them +/// +/// Key characteristics: +/// - Relative volatility measure +/// - Default periods are 10 and 20 days +/// - Values above 1 indicate increasing volatility +/// - Values below 1 indicate decreasing volatility +/// - Normalized comparison +/// +/// Formula: +/// Short Volatility = StdDev(Returns, shortPeriod) +/// Long Volatility = StdDev(Returns, longPeriod) +/// VR = Short Volatility / Long Volatility +/// +/// Market Applications: +/// - Volatility regime changes +/// - Market condition analysis +/// - Risk assessment +/// - Trading strategy adaptation +/// - Trend confirmation +/// +/// Note: Values significantly different from 1 indicate changing market conditions +/// + +[SkipLocalsInit] +public sealed class Vr : AbstractBase +{ + private readonly int _longPeriod; + private readonly CircularBuffer _shortReturns; + private readonly CircularBuffer _longReturns; + private double _prevClose; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vr(int shortPeriod = 10, int longPeriod = 20) + { + _longPeriod = longPeriod; + WarmupPeriod = longPeriod + 1; // Need one extra period for returns + Name = $"VR({shortPeriod},{_longPeriod})"; + _shortReturns = new CircularBuffer(shortPeriod); + _longReturns = new CircularBuffer(longPeriod); + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vr(object source, int shortPeriod = 10, int longPeriod = 20) : this(shortPeriod, longPeriod) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prevClose = 0; + _shortReturns.Clear(); + _longReturns.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private double CalculateVariance(CircularBuffer buffer) + { + if (buffer.Count == 0) return 0; + double mean = buffer.Average(); + double sumSquaredDiff = 0; + for (int i = 0; i < buffer.Count; i++) + { + double diff = buffer[i] - mean; + sumSquaredDiff += diff * diff; + } + return sumSquaredDiff / buffer.Count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Skip first period to establish previous close + if (_index == 1) + { + _prevClose = BarInput.Close; + return 0; + } + + // Calculate return + double ret = _prevClose > double.Epsilon ? Math.Log(BarInput.Close / _prevClose) : 0; + + // Add return to buffers + _shortReturns.Add(ret); + _longReturns.Add(ret); + + // Store current close for next calculation + _prevClose = BarInput.Close; + + // Need enough returns for both periods + if (_index <= _longPeriod) + { + return 0; + } + + // Calculate volatilities + double shortVol = Math.Sqrt(CalculateVariance(_shortReturns)); + double longVol = Math.Sqrt(CalculateVariance(_longReturns)); + + // Calculate ratio + double vr = longVol > double.Epsilon ? shortVol / longVol : 1; + + IsHot = _index >= WarmupPeriod; + return vr; + } +} diff --git a/lib/volatility/Vs.cs b/lib/volatility/Vs.cs new file mode 100644 index 00000000..aa19b409 --- /dev/null +++ b/lib/volatility/Vs.cs @@ -0,0 +1,155 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// VS: Volatility Stop +/// A technical indicator that uses volatility to determine stop levels, +/// adapting to market conditions for dynamic risk management. +/// +/// +/// The VS calculation process: +/// 1. Calculate Average True Range (ATR) +/// 2. Calculate stop levels: +/// Long Stop = Close - (multiplier * ATR) +/// Short Stop = Close + (multiplier * ATR) +/// 3. Trail stops based on price movement +/// +/// Key characteristics: +/// - Adaptive stop levels +/// - Based on ATR volatility +/// - Default period is 14 days +/// - Returns both long and short stops +/// - Trails with price movement +/// +/// Formula: +/// ATR = Average(TR, period) +/// Long Stop = Close - (multiplier * ATR) +/// Short Stop = Close + (multiplier * ATR) +/// +/// Market Applications: +/// - Stop loss placement +/// - Position management +/// - Risk control +/// - Trend following +/// - Exit strategy +/// +/// Sources: +/// Adaptation of Volatility-Based Stops concept +/// https://www.investopedia.com/terms/v/volatility-stop.asp +/// +/// Note: Returns two values: long stop and short stop levels +/// + +[SkipLocalsInit] +public sealed class Vs : AbstractBase +{ + private readonly int _period; + private readonly double _multiplier; + private readonly CircularBuffer _tr; + private double _prevClose; + private double _longStop; + private double _shortStop; + private double _prevLongStop; + private double _prevShortStop; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vs(int period = 14, double multiplier = 2.0) + { + _period = period; + _multiplier = multiplier; + WarmupPeriod = period + 1; // Need one extra period for TR + Name = $"VS({_period},{_multiplier})"; + _tr = new CircularBuffer(period); + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vs(object source, int period = 14, double multiplier = 2.0) : this(period, multiplier) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prevClose = 0; + _longStop = 0; + _shortStop = 0; + _prevLongStop = 0; + _prevShortStop = 0; + _tr.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Skip first period to establish previous close + if (_index == 1) + { + _prevClose = BarInput.Close; + _longStop = BarInput.Close; + _shortStop = BarInput.Close; + return 0; + } + + // Calculate True Range + double tr = Math.Max(BarInput.High - BarInput.Low, + Math.Max(Math.Abs(BarInput.High - _prevClose), + Math.Abs(BarInput.Low - _prevClose))); + + // Add TR to buffer + _tr.Add(tr); + + // Store current close for next calculation + _prevClose = BarInput.Close; + + // Need enough values for ATR calculation + if (_index <= _period) + { + return 0; + } + + // Calculate ATR + double atr = _tr.Average(); + + // Calculate initial stop levels + double potentialLongStop = BarInput.Close - (_multiplier * atr); + double potentialShortStop = BarInput.Close + (_multiplier * atr); + + // Trail stops + _longStop = BarInput.Close > _prevShortStop ? potentialLongStop : Math.Max(potentialLongStop, _prevLongStop); + _shortStop = BarInput.Close < _prevLongStop ? potentialShortStop : Math.Min(potentialShortStop, _prevShortStop); + + // Store current stops for next calculation + _prevLongStop = _longStop; + _prevShortStop = _shortStop; + + IsHot = _index >= WarmupPeriod; + return _longStop; // Return long stop as primary value + } + + /// + /// Gets the long stop level + /// + public double LongStop => _longStop; + + /// + /// Gets the short stop level + /// + public double ShortStop => _shortStop; +} diff --git a/lib/volatility/_list.md b/lib/volatility/_list.md index 93598c02..65243f2c 100644 --- a/lib/volatility/_list.md +++ b/lib/volatility/_list.md @@ -1,37 +1,38 @@ # Volatility indicators -Done: 6, Todo: 25 +Done: 11, Todo: 24 ADR - Average Daily Range AP - Andrew's Pitchfork ✔️ ATR - Average True Range ATRP - Average True Range Percent ATRS - ATR Trailing Stop -BB - Bollinger Bands® +*BB - Bollinger Bands® (Upper, Middle, Lower) CCV - Close-to-Close Volatility CE - Chandelier Exit CV - Conditional Volatility (ARCH/GARCH) CVI - Chaikin's Volatility -DC - Donchian Channels +*DC - Donchian Channels (Upper, Middle, Lower) +EWMA - Exponential Weighted Moving Average Volatility FCB - Fractal Chaos Bands GKV - Garman-Klass Volatility HLV - High-Low Volatility ✔️ HV - Historical Volatility -ICH - Ichimoku Cloud +*ICH - Ichimoku Cloud (Conversion, Base, Leading Span A, Leading Span B, Lagging Span) ✔️ JVOLTY - Jurik Volatility -KC - Keltner Channels +*KC - Keltner Channels (Upper, Middle, Lower) NATR - Normalized Average True Range PCH - Price Channel Indicator -PSAR - Parabolic Stop and Reverse +*PSAR - Parabolic Stop and Reverse (Value, Trend) PV - Parkinson Volatility RSV - Rogers-Satchell Volatility ✔️ RV - Realized Volatility ✔️ RVI - Relative Volatility Index -STARC - Starc Bands +*STARC - Starc Bands (Upper, Middle, Lower) SV - Stochastic Volatility -TR - True Range -UI - Ulcer Index -VC - Volatility Cone -VOV - Volatility of Volatility -VR - Volatility Ratio -VS - Volatility Stop +✔️ TR - True Range +✔️ UI - Ulcer Index +✔️ *VC - Volatility Cone (Mean, Upper Bound, Lower Bound) +✔️ VOV - Volatility of Volatility +✔️ VR - Volatility Ratio +✔️ *VS - Volatility Stop (Long Stop, Short Stop) YZV - Yang-Zhang Volatility diff --git a/lib/volatility/todo.md b/lib/volatility/todo.md deleted file mode 100644 index 32636c3f..00000000 --- a/lib/volatility/todo.md +++ /dev/null @@ -1,29 +0,0 @@ -# Volatility Measures - -## Single Value Input (Typically Closing Prices) - -- **Jurik Volatility (Volty)** -- **Standard Deviation** -- **RVI Relative Volatility Index** -- **CMO Chande Momentum Oscillator** -- **Historical Volatility** -- **Average True Range (ATR) (High, Low, Close)** - -- Normalized ATR -- Ulcer Index -- ARCH/GARCH Models -- Exponential Weighted Moving Average (EWMA) Volatility -- Conditional Volatility -- Volatility Ratio -- Close-to-Close Volatility -- Volatility of Volatility (VOV) -- Volatility Cone -- Bollinger Bands -- Stochastic Volatility: Typically modeled using closing prices, but can incorporate other price information -- Garman-Klass Volatility -- Rogers-Satchell Volatility -- Yang-Zhang Volatility -- Parkinson Volatility (High, Low) -- Chaikin Volatility (High, Low) -- Keltner Channels (typically Close, High, Low) -- High-Low Volatility (High, Low) diff --git a/lib/volume/Mfi.cs b/lib/volume/Mfi.cs new file mode 100644 index 00000000..2c847630 --- /dev/null +++ b/lib/volume/Mfi.cs @@ -0,0 +1,141 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// MFI: Money Flow Index +/// A volume-weighted momentum indicator that measures the inflow and outflow of money into an asset +/// over a specific period of time. It's sometimes referred to as volume-weighted RSI. +/// +/// +/// The MFI calculation process: +/// 1. Calculate Typical Price: +/// TP = (High + Low + Close) / 3 +/// 2. Calculate Raw Money Flow: +/// RMF = TP * Volume +/// 3. Determine Positive/Negative Money Flow: +/// If TP > Previous TP: Positive Money Flow +/// If TP < Previous TP: Negative Money Flow +/// 4. Calculate Money Flow Ratio: +/// MFR = (14-period Positive Money Flow Sum) / (14-period Negative Money Flow Sum) +/// 5. Calculate Money Flow Index: +/// MFI = 100 - (100 / (1 + MFR)) +/// +/// Key characteristics: +/// - Oscillates between 0 and 100 +/// - Default period is 14 days +/// - Overbought level typically at 80 +/// - Oversold level typically at 20 +/// - Volume-weighted measure +/// +/// Formula: +/// TP = (High + Low + Close) / 3 +/// RMF = TP * Volume +/// MFR = ΣPositive Money Flow / ΣNegative Money Flow +/// MFI = 100 - (100 / (1 + MFR)) +/// +/// Market Applications: +/// - Overbought/Oversold conditions +/// - Divergence analysis +/// - Trend confirmation +/// - Price reversals +/// - Volume flow analysis +/// +/// Sources: +/// Gene Quong and Avrum Soudack - Original development +/// https://www.investopedia.com/terms/m/mfi.asp +/// +/// Note: Values above 80 indicate overbought conditions, while values below 20 indicate oversold conditions +/// + +[SkipLocalsInit] +public sealed class Mfi : AbstractBase +{ + private readonly CircularBuffer _posMf; + private readonly CircularBuffer _negMf; + private double _prevTp; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Mfi(int period = 14) + { + WarmupPeriod = period + 1; // Need one extra period for previous TP + Name = $"MFI({period})"; + _posMf = new CircularBuffer(period); + _negMf = new CircularBuffer(period); + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Mfi(object source, int period = 14) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prevTp = 0; + _posMf.Clear(); + _negMf.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Calculate Typical Price + double tp = (BarInput.High + BarInput.Low + BarInput.Close) / 3; + + // Skip first period to establish previous TP + if (_index == 1) + { + _prevTp = tp; + return 0; + } + + // Calculate Raw Money Flow + double rmf = tp * BarInput.Volume; + + // Determine Positive/Negative Money Flow + if (tp > _prevTp) + { + _posMf.Add(rmf); + _negMf.Add(0); + } + else if (tp < _prevTp) + { + _posMf.Add(0); + _negMf.Add(rmf); + } + else + { + _posMf.Add(0); + _negMf.Add(0); + } + + // Store current TP for next calculation + _prevTp = tp; + + // Calculate Money Flow Ratio and Index + double posMfSum = _posMf.Sum(); + double negMfSum = _negMf.Sum(); + + double mfi = Math.Abs(negMfSum) < double.Epsilon ? 100 : 100 - (100 / (1 + (posMfSum / negMfSum))); + + IsHot = _index >= WarmupPeriod; + return mfi; + } +} diff --git a/lib/volume/Nvi.cs b/lib/volume/Nvi.cs new file mode 100644 index 00000000..e0e1aaf7 --- /dev/null +++ b/lib/volume/Nvi.cs @@ -0,0 +1,114 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// NVI: Negative Volume Index +/// A cumulative indicator that focuses on days when volume decreases from the previous day. +/// It is based on the premise that smart money is active on days with lower volume. +/// +/// +/// The NVI calculation process: +/// 1. Compare current volume with previous volume +/// 2. If current volume is less than previous volume: +/// NVI = Previous NVI + (((Close - Previous Close) / Previous Close) * Previous NVI) +/// 3. If current volume is greater than or equal to previous volume: +/// NVI = Previous NVI +/// +/// Key characteristics: +/// - Cumulative indicator +/// - Only updates on lower volume days +/// - Starts at base value of 1000 +/// - Focuses on smart money activity +/// - Volume-driven measure +/// +/// Formula: +/// If Volume < Previous Volume: +/// NVI = Previous NVI + (Price % Change * Previous NVI) +/// Else: +/// NVI = Previous NVI +/// +/// Market Applications: +/// - Smart money tracking +/// - Trend identification +/// - Market timing +/// - Volume analysis +/// - Price confirmation +/// +/// Sources: +/// Paul Dysart - Original development (1930s) +/// Norman Fosback - Further development +/// https://www.investopedia.com/terms/n/nvi.asp +/// +/// Note: Rising NVI suggests smart money is buying, while falling NVI suggests smart money is selling +/// + +[SkipLocalsInit] +public sealed class Nvi : AbstractBase +{ + private double _prevClose; + private double _prevVolume; + private double _prevNvi; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Nvi() + { + WarmupPeriod = 2; // Need previous volume and close + Name = "NVI"; + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Nvi(object source) : this() + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prevClose = 0; + _prevVolume = 0; + _prevNvi = 1000; // Standard starting value + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Skip first period to establish previous values + if (_index == 1) + { + _prevClose = BarInput.Close; + _prevVolume = BarInput.Volume; + return _prevNvi; + } + + // Calculate NVI + if (BarInput.Volume < _prevVolume) + { + double priceChange = ((BarInput.Close - _prevClose) / _prevClose); + _prevNvi += priceChange * _prevNvi; + } + + // Store current values for next calculation + _prevClose = BarInput.Close; + _prevVolume = BarInput.Volume; + + IsHot = _index >= WarmupPeriod; + return _prevNvi; + } +} diff --git a/lib/volume/Obv.cs b/lib/volume/Obv.cs new file mode 100644 index 00000000..b8189799 --- /dev/null +++ b/lib/volume/Obv.cs @@ -0,0 +1,117 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// OBV: On-Balance Volume +/// A momentum indicator that uses volume flow to predict changes in stock price. +/// It accumulates volume on up days and subtracts volume on down days. +/// +/// +/// The OBV calculation process: +/// 1. Compare current close with previous close +/// 2. If current close is higher: +/// OBV = Previous OBV + Current Volume +/// 3. If current close is lower: +/// OBV = Previous OBV - Current Volume +/// 4. If current close equals previous close: +/// OBV = Previous OBV +/// +/// Key characteristics: +/// - Cumulative indicator +/// - Volume-based momentum measure +/// - Leading indicator +/// - No upper or lower bounds +/// - Focuses on volume flow +/// +/// Formula: +/// If Close > Previous Close: +/// OBV = Previous OBV + Volume +/// If Close < Previous Close: +/// OBV = Previous OBV - Volume +/// If Close = Previous Close: +/// OBV = Previous OBV +/// +/// Market Applications: +/// - Trend confirmation +/// - Potential breakouts +/// - Divergence analysis +/// - Volume flow analysis +/// - Price movement prediction +/// +/// Sources: +/// Joe Granville - Original development (1963) +/// https://www.investopedia.com/terms/o/onbalancevolume.asp +/// +/// Note: Rising OBV suggests buying pressure, while falling OBV suggests selling pressure +/// + +[SkipLocalsInit] +public sealed class Obv : AbstractBase +{ + private double _prevClose; + private double _prevObv; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Obv() + { + WarmupPeriod = 2; // Need previous close + Name = "OBV"; + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Obv(object source) : this() + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prevClose = 0; + _prevObv = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Skip first period to establish previous close + if (_index == 1) + { + _prevClose = BarInput.Close; + return 0; + } + + // Calculate OBV + if (BarInput.Close > _prevClose) + { + _prevObv += BarInput.Volume; + } + else if (BarInput.Close < _prevClose) + { + _prevObv -= BarInput.Volume; + } + // If prices equal, OBV remains the same + + // Store current close for next calculation + _prevClose = BarInput.Close; + + IsHot = _index >= WarmupPeriod; + return _prevObv; + } +} diff --git a/lib/volume/Pvi.cs b/lib/volume/Pvi.cs new file mode 100644 index 00000000..400da515 --- /dev/null +++ b/lib/volume/Pvi.cs @@ -0,0 +1,113 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// PVI: Positive Volume Index +/// A cumulative indicator that focuses on days when volume increases from the previous day. +/// It is based on the premise that the public is active on days with higher volume. +/// +/// +/// The PVI calculation process: +/// 1. Compare current volume with previous volume +/// 2. If current volume is greater than previous volume: +/// PVI = Previous PVI + (((Close - Previous Close) / Previous Close) * Previous PVI) +/// 3. If current volume is less than or equal to previous volume: +/// PVI = Previous PVI +/// +/// Key characteristics: +/// - Cumulative indicator +/// - Only updates on higher volume days +/// - Starts at base value of 1000 +/// - Focuses on public activity +/// - Volume-driven measure +/// +/// Formula: +/// If Volume > Previous Volume: +/// PVI = Previous PVI + (Price % Change * Previous PVI) +/// Else: +/// PVI = Previous PVI +/// +/// Market Applications: +/// - Public participation tracking +/// - Trend identification +/// - Market timing +/// - Volume analysis +/// - Price confirmation +/// +/// Sources: +/// Norman Fosback - Original development +/// https://www.investopedia.com/terms/p/pvi.asp +/// +/// Note: Rising PVI suggests public buying pressure, while falling PVI suggests public selling pressure +/// + +[SkipLocalsInit] +public sealed class Pvi : AbstractBase +{ + private double _prevClose; + private double _prevVolume; + private double _prevPvi; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Pvi() + { + WarmupPeriod = 2; // Need previous volume and close + Name = "PVI"; + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Pvi(object source) : this() + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prevClose = 0; + _prevVolume = 0; + _prevPvi = 1000; // Standard starting value + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Skip first period to establish previous values + if (_index == 1) + { + _prevClose = BarInput.Close; + _prevVolume = BarInput.Volume; + return _prevPvi; + } + + // Calculate PVI + if (BarInput.Volume > _prevVolume) + { + double priceChange = ((BarInput.Close - _prevClose) / _prevClose); + _prevPvi += priceChange * _prevPvi; + } + + // Store current values for next calculation + _prevClose = BarInput.Close; + _prevVolume = BarInput.Volume; + + IsHot = _index >= WarmupPeriod; + return _prevPvi; + } +} diff --git a/lib/volume/Pvo.cs b/lib/volume/Pvo.cs new file mode 100644 index 00000000..bfce2a6f --- /dev/null +++ b/lib/volume/Pvo.cs @@ -0,0 +1,111 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// PVO: Percentage Volume Oscillator +/// A momentum indicator for volume that shows the relationship between two volume moving averages +/// as a percentage. Similar to the Price Oscillator but uses volume instead of price. +/// +/// +/// The PVO calculation process: +/// 1. Calculate short-term EMA of volume +/// 2. Calculate long-term EMA of volume +/// 3. Calculate PVO: +/// PVO = ((Short EMA - Long EMA) / Long EMA) * 100 +/// +/// Key characteristics: +/// - Volume-based momentum indicator +/// - Oscillates around zero +/// - Shows volume trends +/// - Default periods are 12 and 26 days +/// - Percentage-based measure +/// +/// Formula: +/// Short EMA = EMA(Volume, shortPeriod) +/// Long EMA = EMA(Volume, longPeriod) +/// PVO = ((Short EMA - Long EMA) / Long EMA) * 100 +/// +/// Market Applications: +/// - Volume trend analysis +/// - Divergence identification +/// - Volume momentum measurement +/// - Market tops and bottoms +/// - Trading volume patterns +/// +/// Sources: +/// https://www.investopedia.com/terms/p/pvo.asp +/// +/// Note: Positive values indicate higher short-term volume, while negative values indicate higher long-term volume +/// + +[SkipLocalsInit] +public sealed class Pvo : AbstractBase +{ + private readonly int _longPeriod; + private double _shortEma; + private double _longEma; + private readonly double _shortAlpha; + private readonly double _longAlpha; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Pvo(int shortPeriod = 12, int longPeriod = 26) + { + _longPeriod = longPeriod; + WarmupPeriod = longPeriod; + Name = $"PVO({shortPeriod},{_longPeriod})"; + _shortAlpha = 2.0 / (shortPeriod + 1); + _longAlpha = 2.0 / (longPeriod + 1); + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Pvo(object source, int shortPeriod = 12, int longPeriod = 26) : this(shortPeriod, longPeriod) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _shortEma = 0; + _longEma = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Initialize or update EMAs + if (_index <= _longPeriod) + { + _shortEma = BarInput.Volume; + _longEma = BarInput.Volume; + return 0; + } + + // Update EMAs + _shortEma = (_shortAlpha * BarInput.Volume) + ((1 - _shortAlpha) * _shortEma); + _longEma = (_longAlpha * BarInput.Volume) + ((1 - _longAlpha) * _longEma); + + // Calculate PVO + + double pvo = Math.Abs(_longEma) >= double.Epsilon ? ((_shortEma - _longEma) / _longEma) * 100 : 0; + + IsHot = _index >= WarmupPeriod; + return pvo; + } +} diff --git a/lib/volume/Pvol.cs b/lib/volume/Pvol.cs new file mode 100644 index 00000000..f9c6d975 --- /dev/null +++ b/lib/volume/Pvol.cs @@ -0,0 +1,108 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// PVOL: Price-Volume +/// A technical indicator that measures the relationship between price and volume changes, +/// helping to identify the strength of price movements. +/// +/// +/// The PVOL calculation process: +/// 1. Calculate price change: +/// Price Change = (Close - Previous Close) / Previous Close +/// 2. Calculate volume change: +/// Volume Change = (Volume - Previous Volume) / Previous Volume +/// 3. Calculate PVOL: +/// PVOL = Price Change * Volume Change * 100 +/// +/// Key characteristics: +/// - Measures price-volume relationship +/// - Oscillates around zero +/// - Shows momentum strength +/// - Identifies volume-supported moves +/// - No specific boundaries +/// +/// Formula: +/// Price Change = (Close - Previous Close) / Previous Close +/// Volume Change = (Volume - Previous Volume) / Previous Volume +/// PVOL = Price Change * Volume Change * 100 +/// +/// Market Applications: +/// - Price movement confirmation +/// - Volume analysis +/// - Trend strength assessment +/// - Divergence identification +/// - Market momentum analysis +/// +/// Note: High positive values indicate strong upward momentum with volume support, +/// while high negative values indicate strong downward momentum with volume support +/// + +[SkipLocalsInit] +public sealed class Pvol : AbstractBase +{ + private double _prevClose; + private double _prevVolume; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Pvol() + { + WarmupPeriod = 2; // Need previous close and volume + Name = "PVOL"; + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Pvol(object source) : this() + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prevClose = 0; + _prevVolume = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Skip first period to establish previous values + if (_index == 1) + { + _prevClose = BarInput.Close; + _prevVolume = BarInput.Volume; + return 0; + } + + // Calculate price and volume changes + double priceChange = (Math.Abs(_prevClose) >= double.Epsilon) ? (BarInput.Close - _prevClose) / _prevClose : 0; + double volumeChange = (Math.Abs(_prevVolume) >= double.Epsilon) ? (BarInput.Volume - _prevVolume) / _prevVolume : 0; + + // Store current values for next calculation + _prevClose = BarInput.Close; + _prevVolume = BarInput.Volume; + + // Calculate PVOL + double pvol = priceChange * volumeChange * 100; + + IsHot = _index >= WarmupPeriod; + return pvol; + } +} diff --git a/lib/volume/Pvr.cs b/lib/volume/Pvr.cs new file mode 100644 index 00000000..702029f7 --- /dev/null +++ b/lib/volume/Pvr.cs @@ -0,0 +1,109 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// PVR: Price Volume Rank +/// A technical indicator that ranks price and volume movements to identify +/// significant market moves based on their combined strength. +/// +/// +/// The PVR calculation process: +/// 1. Calculate price change percentage: +/// Price Change = ((Close - Previous Close) / Previous Close) * 100 +/// 2. Calculate volume ratio: +/// Volume Ratio = Current Volume / Previous Volume +/// 3. Calculate PVR: +/// PVR = Price Change * Volume Ratio +/// +/// Key characteristics: +/// - Combines price and volume analysis +/// - No specific boundaries +/// - Measures movement significance +/// - Volume-weighted price change +/// - Identifies strong moves +/// +/// Formula: +/// Price Change = ((Close - Previous Close) / Previous Close) * 100 +/// Volume Ratio = Volume / Previous Volume +/// PVR = Price Change * Volume Ratio +/// +/// Market Applications: +/// - Significant move identification +/// - Volume-supported moves +/// - Trend strength analysis +/// - Breakout confirmation +/// - Market momentum measurement +/// +/// Note: Higher absolute values indicate more significant price moves with volume support +/// + +[SkipLocalsInit] +public sealed class Pvr : AbstractBase +{ + private double _prevClose; + private double _prevVolume; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Pvr() + { + WarmupPeriod = 2; // Need previous close and volume + Name = "PVR"; + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Pvr(object source) : this() + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prevClose = 0; + _prevVolume = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Skip first period to establish previous values + if (_index == 1) + { + _prevClose = BarInput.Close; + _prevVolume = BarInput.Volume; + return 0; + } + + // Calculate price change percentage + double priceChange = (Math.Abs(_prevClose) > double.Epsilon) ? ((BarInput.Close - _prevClose) / _prevClose) * 100 : 0; + + // Calculate volume ratio + double volumeRatio = (Math.Abs(_prevVolume) > double.Epsilon) ? BarInput.Volume / _prevVolume : 1; + + // Store current values for next calculation + _prevClose = BarInput.Close; + _prevVolume = BarInput.Volume; + + // Calculate PVR + double pvr = priceChange * volumeRatio; + + IsHot = _index >= WarmupPeriod; + return pvr; + } +} diff --git a/lib/volume/Pvt.cs b/lib/volume/Pvt.cs new file mode 100644 index 00000000..150bf855 --- /dev/null +++ b/lib/volume/Pvt.cs @@ -0,0 +1,105 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// PVT: Price Volume Trend +/// A momentum indicator that combines price and volume to determine the strength of a trend. +/// Similar to OBV but uses percentage price changes in its calculation. +/// +/// +/// The PVT calculation process: +/// 1. Calculate price change percentage: +/// Price Change = (Close - Previous Close) / Previous Close +/// 2. Calculate PVT: +/// PVT = Previous PVT + (Price Change * Volume) +/// +/// Key characteristics: +/// - Cumulative indicator +/// - Volume-weighted price changes +/// - No upper or lower bounds +/// - Trend strength measure +/// - More sensitive than OBV +/// +/// Formula: +/// Price Change = (Close - Previous Close) / Previous Close +/// PVT = Previous PVT + (Price Change * Volume) +/// +/// Market Applications: +/// - Trend confirmation +/// - Divergence analysis +/// - Volume-price relationships +/// - Support/resistance levels +/// - Market momentum +/// +/// Sources: +/// Norman Fosback - Original development +/// https://www.investopedia.com/terms/p/pvt.asp +/// +/// Note: Rising PVT suggests buying pressure, while falling PVT suggests selling pressure +/// + +[SkipLocalsInit] +public sealed class Pvt : AbstractBase +{ + private double _prevClose; + private double _prevPvt; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Pvt() + { + WarmupPeriod = 2; // Need previous close + Name = "PVT"; + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Pvt(object source) : this() + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prevClose = 0; + _prevPvt = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Skip first period to establish previous close + if (_index == 1) + { + _prevClose = BarInput.Close; + return 0; + } + + // Calculate price change percentage + double priceChange = (Math.Abs(_prevClose) > double.Epsilon) ? (BarInput.Close - _prevClose) / _prevClose : 0; + + // Calculate PVT + _prevPvt += priceChange * BarInput.Volume; + + // Store current close for next calculation + _prevClose = BarInput.Close; + + IsHot = _index >= WarmupPeriod; + return _prevPvt; + } +} diff --git a/lib/volume/Tvi.cs b/lib/volume/Tvi.cs new file mode 100644 index 00000000..864dec45 --- /dev/null +++ b/lib/volume/Tvi.cs @@ -0,0 +1,112 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// TVI: Trade Volume Index +/// A technical indicator that determines whether a security is being accumulated or distributed +/// based on price changes relative to a minimum tick value. +/// +/// +/// The TVI calculation process: +/// 1. Calculate price change: +/// Price Change = Close - Previous Close +/// 2. Compare price change to minimum tick value: +/// If |Price Change| >= Minimum Tick: +/// Add/Subtract volume based on price direction +/// +/// Key characteristics: +/// - Volume-based trend indicator +/// - Uses minimum tick value +/// - Cumulative measure +/// - No upper or lower bounds +/// - Focuses on significant moves +/// +/// Formula: +/// If |Close - Previous Close| >= Minimum Tick: +/// If Close > Previous Close: +/// TVI = Previous TVI + Volume +/// If Close < Previous Close: +/// TVI = Previous TVI - Volume +/// Else: +/// TVI = Previous TVI +/// +/// Market Applications: +/// - Trend identification +/// - Volume analysis +/// - Accumulation/distribution +/// - Price movement significance +/// - Trading signal generation +/// +/// Note: Rising TVI suggests accumulation, while falling TVI suggests distribution +/// + +[SkipLocalsInit] +public sealed class Tvi : AbstractBase +{ + private readonly double _minTick; + private double _prevClose; + private double _prevTvi; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Tvi(double minTick = 0.5) + { + _minTick = minTick; + WarmupPeriod = 2; // Need previous close + Name = $"TVI({_minTick})"; + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Tvi(object source, double minTick = 0.5) : this(minTick) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prevClose = 0; + _prevTvi = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Skip first period to establish previous close + if (_index == 1) + { + _prevClose = BarInput.Close; + return 0; + } + + // Calculate price change + double priceChange = BarInput.Close - _prevClose; + + // Update TVI if price change exceeds minimum tick + if (Math.Abs(priceChange) >= _minTick) + { + _prevTvi += priceChange > 0 ? BarInput.Volume : -BarInput.Volume; + } + + // Store current close for next calculation + _prevClose = BarInput.Close; + + IsHot = _index >= WarmupPeriod; + return _prevTvi; + } +} diff --git a/lib/volume/_list.md b/lib/volume/_list.md index 54149915..8fdb2f14 100644 --- a/lib/volume/_list.md +++ b/lib/volume/_list.md @@ -1,5 +1,5 @@ # Volume indicators -Done: 6, Todo: 12 +Done: 15, Todo: 3 ✔️ ADL - Chaikin Accumulation Distribution Line ✔️ ADOSC - Chaikin Accumulation Distribution Oscillator @@ -7,15 +7,16 @@ Done: 6, Todo: 12 ✔️ CMF - Chaikin Money Flow ✔️ EOM - Ease of Movement ✔️ KVO - Klinger Volume Oscillator -MFI - Money Flow Index -NVI - Negative Volume Index -OBV - On-Balance Volume -PVI - Positive Volume Index -PVOL - Price-Volume -PVO - Percentage Volume Oscillator -PVR - Price Volume Rank -PVT - Price Volume Trend -TVI - Trade Volume Index +✔️ MFI - Money Flow Index +✔️ NVI - Negative Volume Index +✔️ OBV - On-Balance Volume +✔️ PVI - Positive Volume Index +✔️ PVOL - Price-Volume +✔️ PVO - Percentage Volume Oscillator +✔️ PVR - Price Volume Rank +✔️ PVT - Price Volume Trend +✔️ TVI - Trade Volume Index +VF - Volume Force VP - Volume Profile VWAP - Volume Weighted Average Price VWMA - Volume Weighted Moving Average From 447d90f4d6c902f652bb358b64195749f14ff1f8 Mon Sep 17 00:00:00 2001 From: Miha Date: Thu, 31 Oct 2024 09:49:06 -0700 Subject: [PATCH 12/17] docs --- docs/_sidebar.md | 21 +- docs/index.html | 36 +- .../averages/convolution/convolution.md | 0 docs/indicators/averages/dwma/dwma.md | 0 docs/indicators/averages/epma/epma.md | 0 docs/indicators/averages/frama/frama.md | 0 docs/indicators/averages/fwma/fwma.md | 0 docs/indicators/averages/gma/gma.md | 0 docs/indicators/averages/hma/hma.md | 0 docs/indicators/averages/htit/htit.md | 0 docs/indicators/averages/hwma/hwma.md | 0 docs/indicators/averages/jma/jma.md | 0 docs/indicators/averages/kama/kama.md | 0 docs/indicators/averages/ltma/ltma.md | 0 docs/indicators/averages/maaf/maaf.md | 0 docs/indicators/averages/mama/mama.md | 0 docs/indicators/averages/mgdi/mgdi.md | 0 docs/indicators/averages/mma/mma.md | 0 docs/indicators/averages/pwma/pwma.md | 0 docs/indicators/averages/qema/qema.md | 0 docs/indicators/averages/rema/rema.md | 0 docs/indicators/averages/rma/rma.md | 0 docs/indicators/averages/sinema/sinema.md | 0 docs/indicators/averages/sma/sma.md | 0 docs/indicators/averages/smma/smma.md | 0 docs/indicators/averages/t3/t3.md | 0 docs/indicators/averages/tema/tema.md | 0 docs/indicators/averages/trima/trima.md | 0 docs/indicators/averages/vidya/vidya.md | 0 docs/indicators/averages/wma/wma.md | 0 docs/indicators/averages/zlema/zlema.md | 0 docs/indicators/indicators.md | 10 +- docs/indicators/momentum/adx/description.md | 43 + docs/indicators/momentum/adxr/description.md | 43 + docs/indicators/momentum/apo/description.md | 43 + docs/indicators/momentum/dmi/description.md | 43 + docs/indicators/momentum/dmx/description.md | 20 + docs/indicators/momentum/dpo/description.md | 43 + docs/indicators/momentum/mom/description.md | 43 + docs/indicators/momentum/pmo/description.md | 43 + docs/indicators/momentum/po/description.md | 43 + docs/indicators/momentum/ppo/description.md | 48 + docs/indicators/momentum/prs/description.md | 48 + docs/indicators/momentum/roc/description.md | 48 + docs/indicators/momentum/trix/description.md | 48 + docs/indicators/momentum/tsi/description.md | 48 + docs/indicators/momentum/vel/description.md | 43 + .../indicators/momentum/vortex/description.md | 48 + docs/indicators/oscillators/ac/ac.md | 0 docs/indicators/oscillators/ao/ao.md | 0 docs/indicators/oscillators/aroon/aroon.md | 0 docs/indicators/oscillators/bop/bop.md | 0 docs/indicators/oscillators/cci/cci.md | 0 docs/indicators/oscillators/cfo/cfo.md | 0 docs/indicators/oscillators/cmo/cmo.md | 0 docs/indicators/oscillators/rsi/rsi.md | 0 docs/indicators/oscillators/rsx/rsx.md | 0 .../statistics/curvature/curvature.md | 0 docs/indicators/statistics/entropy/entropy.md | 0 .../statistics/kurtosis/kurtosis.md | 0 docs/indicators/statistics/max/max.md | 0 docs/indicators/statistics/median/median.md | 0 docs/indicators/statistics/min/min.md | 0 docs/indicators/statistics/mode/mode.md | 0 .../statistics/percentile/percentile.md | 0 docs/indicators/statistics/skew/skew.md | 0 docs/indicators/statistics/slope/slope.md | 0 docs/indicators/statistics/stddev/stddev.md | 0 .../statistics/variance/variance.md | 0 docs/indicators/statistics/zscore/zscore.md | 0 docs/indicators/volatility/atr/atr.md | 0 docs/indicators/volatility/hv/hv.md | 0 docs/indicators/volatility/jvolty/jvolty.md | 0 docs/indicators/volatility/rv/rv.md | 0 docs/indicators/volatility/tr/tr.md | 0 docs/indicators/volatility/ui/ui.md | 0 docs/indicators/volatility/vc/vc.md | 0 docs/indicators/volatility/vov/vov.md | 0 docs/indicators/volatility/vr/vr.md | 0 docs/indicators/volatility/vs/vs.md | 0 docs/indicators/volume/adl/adl.md | 0 docs/indicators/volume/adosc/adosc.md | 0 docs/indicators/volume/aobv/aobv.md | 0 docs/indicators/volume/cmf/cmf.md | 0 docs/indicators/volume/eom/eom.md | 0 docs/indicators/volume/kvo/kvo.md | 0 docs/indicators/volume/mfi/mfi.md | 0 docs/indicators/volume/nvi/nvi.md | 0 docs/indicators/volume/obv/obv.md | 0 docs/indicators/volume/pvi/pvi.md | 0 docs/indicators/volume/pvo/pvo.md | 0 docs/indicators/volume/pvol/pvol.md | 0 docs/indicators/volume/pvr/pvr.md | 0 docs/indicators/volume/pvt/pvt.md | 0 docs/indicators/volume/tvi/tvi.md | 0 docs/styles.css | 1617 ----------------- lib/oscillators/_list.md | 2 +- omnisharp.json | 19 + 98 files changed, 749 insertions(+), 1651 deletions(-) create mode 100644 docs/indicators/averages/convolution/convolution.md create mode 100644 docs/indicators/averages/dwma/dwma.md create mode 100644 docs/indicators/averages/epma/epma.md create mode 100644 docs/indicators/averages/frama/frama.md create mode 100644 docs/indicators/averages/fwma/fwma.md create mode 100644 docs/indicators/averages/gma/gma.md create mode 100644 docs/indicators/averages/hma/hma.md create mode 100644 docs/indicators/averages/htit/htit.md create mode 100644 docs/indicators/averages/hwma/hwma.md create mode 100644 docs/indicators/averages/jma/jma.md create mode 100644 docs/indicators/averages/kama/kama.md create mode 100644 docs/indicators/averages/ltma/ltma.md create mode 100644 docs/indicators/averages/maaf/maaf.md create mode 100644 docs/indicators/averages/mama/mama.md create mode 100644 docs/indicators/averages/mgdi/mgdi.md create mode 100644 docs/indicators/averages/mma/mma.md create mode 100644 docs/indicators/averages/pwma/pwma.md create mode 100644 docs/indicators/averages/qema/qema.md create mode 100644 docs/indicators/averages/rema/rema.md create mode 100644 docs/indicators/averages/rma/rma.md create mode 100644 docs/indicators/averages/sinema/sinema.md create mode 100644 docs/indicators/averages/sma/sma.md create mode 100644 docs/indicators/averages/smma/smma.md create mode 100644 docs/indicators/averages/t3/t3.md create mode 100644 docs/indicators/averages/tema/tema.md create mode 100644 docs/indicators/averages/trima/trima.md create mode 100644 docs/indicators/averages/vidya/vidya.md create mode 100644 docs/indicators/averages/wma/wma.md create mode 100644 docs/indicators/averages/zlema/zlema.md create mode 100644 docs/indicators/momentum/adx/description.md create mode 100644 docs/indicators/momentum/adxr/description.md create mode 100644 docs/indicators/momentum/apo/description.md create mode 100644 docs/indicators/momentum/dmi/description.md create mode 100644 docs/indicators/momentum/dmx/description.md create mode 100644 docs/indicators/momentum/dpo/description.md create mode 100644 docs/indicators/momentum/mom/description.md create mode 100644 docs/indicators/momentum/pmo/description.md create mode 100644 docs/indicators/momentum/po/description.md create mode 100644 docs/indicators/momentum/ppo/description.md create mode 100644 docs/indicators/momentum/prs/description.md create mode 100644 docs/indicators/momentum/roc/description.md create mode 100644 docs/indicators/momentum/trix/description.md create mode 100644 docs/indicators/momentum/tsi/description.md create mode 100644 docs/indicators/momentum/vel/description.md create mode 100644 docs/indicators/momentum/vortex/description.md create mode 100644 docs/indicators/oscillators/ac/ac.md create mode 100644 docs/indicators/oscillators/ao/ao.md create mode 100644 docs/indicators/oscillators/aroon/aroon.md create mode 100644 docs/indicators/oscillators/bop/bop.md create mode 100644 docs/indicators/oscillators/cci/cci.md create mode 100644 docs/indicators/oscillators/cfo/cfo.md create mode 100644 docs/indicators/oscillators/cmo/cmo.md create mode 100644 docs/indicators/oscillators/rsi/rsi.md create mode 100644 docs/indicators/oscillators/rsx/rsx.md create mode 100644 docs/indicators/statistics/curvature/curvature.md create mode 100644 docs/indicators/statistics/entropy/entropy.md create mode 100644 docs/indicators/statistics/kurtosis/kurtosis.md create mode 100644 docs/indicators/statistics/max/max.md create mode 100644 docs/indicators/statistics/median/median.md create mode 100644 docs/indicators/statistics/min/min.md create mode 100644 docs/indicators/statistics/mode/mode.md create mode 100644 docs/indicators/statistics/percentile/percentile.md create mode 100644 docs/indicators/statistics/skew/skew.md create mode 100644 docs/indicators/statistics/slope/slope.md create mode 100644 docs/indicators/statistics/stddev/stddev.md create mode 100644 docs/indicators/statistics/variance/variance.md create mode 100644 docs/indicators/statistics/zscore/zscore.md create mode 100644 docs/indicators/volatility/atr/atr.md create mode 100644 docs/indicators/volatility/hv/hv.md create mode 100644 docs/indicators/volatility/jvolty/jvolty.md create mode 100644 docs/indicators/volatility/rv/rv.md create mode 100644 docs/indicators/volatility/tr/tr.md create mode 100644 docs/indicators/volatility/ui/ui.md create mode 100644 docs/indicators/volatility/vc/vc.md create mode 100644 docs/indicators/volatility/vov/vov.md create mode 100644 docs/indicators/volatility/vr/vr.md create mode 100644 docs/indicators/volatility/vs/vs.md create mode 100644 docs/indicators/volume/adl/adl.md create mode 100644 docs/indicators/volume/adosc/adosc.md create mode 100644 docs/indicators/volume/aobv/aobv.md create mode 100644 docs/indicators/volume/cmf/cmf.md create mode 100644 docs/indicators/volume/eom/eom.md create mode 100644 docs/indicators/volume/kvo/kvo.md create mode 100644 docs/indicators/volume/mfi/mfi.md create mode 100644 docs/indicators/volume/nvi/nvi.md create mode 100644 docs/indicators/volume/obv/obv.md create mode 100644 docs/indicators/volume/pvi/pvi.md create mode 100644 docs/indicators/volume/pvo/pvo.md create mode 100644 docs/indicators/volume/pvol/pvol.md create mode 100644 docs/indicators/volume/pvr/pvr.md create mode 100644 docs/indicators/volume/pvt/pvt.md create mode 100644 docs/indicators/volume/tvi/tvi.md delete mode 100644 docs/styles.css create mode 100644 omnisharp.json diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 1d6e0c3e..9c8f64e6 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -1,7 +1,6 @@ - * [Home](/) -[JMA](indicators/averages/jma/calc.md) +* [Available Indicators](indicators/indicators.md) * Introduction * [Overview]() @@ -28,6 +27,23 @@ * [Inspecting Quantower Internals]() * [Available Indicators](indicators/indicators.md) + * Momentum + * [ADX - Average Directional Index](indicators/momentum/adx/description.md) + * [ADXR - Average Directional Index Rating](indicators/momentum/adxr/description.md) + * [APO - Absolute Price Oscillator](indicators/momentum/apo/description.md) + * [DMI - Directional Movement Index](indicators/momentum/dmi/description.md) + * [DMX - Directional Movement Extended](indicators/momentum/dmx/description.md) + * [DPO - Detrended Price Oscillator](indicators/momentum/dpo/description.md) + * [MOM - Momentum](indicators/momentum/mom/description.md) + * [PMO - Price Momentum Oscillator](indicators/momentum/pmo/description.md) + * [PO - Price Oscillator](indicators/momentum/po/description.md) + * [PPO - Percentage Price Oscillator](indicators/momentum/ppo/description.md) + * [PRS - Price Relative Strength](indicators/momentum/prs/description.md) + * [ROC - Rate of Change](indicators/momentum/roc/description.md) + * [TRIX - Triple Exponential](indicators/momentum/trix/description.md) + * [TSI - True Strength Index](indicators/momentum/tsi/description.md) + * [VEL - Velocity](indicators/momentum/vel/description.md) + * [VORTEX - Vortex Indicator](indicators/momentum/vortex/description.md) * Basic Transforms * Numerical Analysis * Errors @@ -95,4 +111,3 @@ * Oscillators * Volatility * Volume - diff --git a/docs/index.html b/docs/index.html index eb060953..00fb80ce 100644 --- a/docs/index.html +++ b/docs/index.html @@ -7,7 +7,7 @@ - +
@@ -19,28 +19,20 @@ loadSidebar: true, auto2top: true, mergeNavbar: true, - sidebarDisplayLevel: 0, // Add this line to collapse all sidebar items initially - search: { - maxAge: 86400000, - paths: 'auto', - placeholder: 'Type to search', - noData: 'No Results!', - depth: 6, - hideOtherSidebarContent: false, - }, - pagination: { - previousText: 'Previous', - nextText: 'Next', - crossChapter: true, - crossChapterText: true, - }, + sidebarDisplayLevel: 0, + maxAge: 86400000, + paths: 'auto', + placeholder: 'Type to search', + noData: 'No Results!', + depth: 6, + hideOtherSidebarContent: false, themeable: { readyTransition: true, responsiveTables: true }, latex: { inlineMath: [['$', '$'], ['\\(', '\\)']], - displayMath: [['$$', '$$']], + displayMath: [['$$', '$$']] }, executeScript: true, externalLinkTarget: '_blank', @@ -49,15 +41,9 @@ toc: { scope: '.markdown-section', headings: 'h1, h2, h3, h4, h5, h6', - title: 'Table of Contents', + title: 'Table of Contents' }, - copyCode: { - buttonText: 'Copy to clipboard', - errorText: 'Error', - successText: 'Copied' - }, - plugins: [ - ] + plugins: [] }; diff --git a/docs/indicators/averages/convolution/convolution.md b/docs/indicators/averages/convolution/convolution.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/dwma/dwma.md b/docs/indicators/averages/dwma/dwma.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/epma/epma.md b/docs/indicators/averages/epma/epma.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/frama/frama.md b/docs/indicators/averages/frama/frama.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/fwma/fwma.md b/docs/indicators/averages/fwma/fwma.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/gma/gma.md b/docs/indicators/averages/gma/gma.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/hma/hma.md b/docs/indicators/averages/hma/hma.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/htit/htit.md b/docs/indicators/averages/htit/htit.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/hwma/hwma.md b/docs/indicators/averages/hwma/hwma.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/jma/jma.md b/docs/indicators/averages/jma/jma.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/kama/kama.md b/docs/indicators/averages/kama/kama.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/ltma/ltma.md b/docs/indicators/averages/ltma/ltma.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/maaf/maaf.md b/docs/indicators/averages/maaf/maaf.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/mama/mama.md b/docs/indicators/averages/mama/mama.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/mgdi/mgdi.md b/docs/indicators/averages/mgdi/mgdi.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/mma/mma.md b/docs/indicators/averages/mma/mma.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/pwma/pwma.md b/docs/indicators/averages/pwma/pwma.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/qema/qema.md b/docs/indicators/averages/qema/qema.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/rema/rema.md b/docs/indicators/averages/rema/rema.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/rma/rma.md b/docs/indicators/averages/rma/rma.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/sinema/sinema.md b/docs/indicators/averages/sinema/sinema.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/sma/sma.md b/docs/indicators/averages/sma/sma.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/smma/smma.md b/docs/indicators/averages/smma/smma.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/t3/t3.md b/docs/indicators/averages/t3/t3.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/tema/tema.md b/docs/indicators/averages/tema/tema.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/trima/trima.md b/docs/indicators/averages/trima/trima.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/vidya/vidya.md b/docs/indicators/averages/vidya/vidya.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/wma/wma.md b/docs/indicators/averages/wma/wma.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/averages/zlema/zlema.md b/docs/indicators/averages/zlema/zlema.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/indicators.md b/docs/indicators/indicators.md index 6848ff29..35bd8401 100644 --- a/docs/indicators/indicators.md +++ b/docs/indicators/indicators.md @@ -11,19 +11,19 @@ - Volume: 15 of 19 complete - Numerical Analysis: 13 of 20 complete - Errors: 16 of 16 complete -- **Total: 116 of 175 indicators implemented (66%)** +- Total: 116 of 175 indicators implemented (66%) -|**BASIC TRANSFORMS**|**Class Name**| -|--|:--:| + +|BASIC TRANSFORMS|Class Name| +|---|:--:| |OC2 - Midpoint price|`.OC2`| |HL2 - Median Price|`.HL2`| |HLC3 - Typical Price|`.HLC3`| |OHL3 - Mean Price|`.OHL3`| |OHLC4 - Average Price|`.OHLC4`| |HLCC4 - Weighted Price|`.HLCC4`| - +
|**AVERAGES & TRENDS**|**Class Name**| -|--|:--:| |AFIRMA - Adaptive FIR Moving Average|`Afirma`| |ALMA - Arnaud Legoux Moving Average|`Alma`| |DEMA - Double Exponential Moving Average|`Dema`| diff --git a/docs/indicators/momentum/adx/description.md b/docs/indicators/momentum/adx/description.md new file mode 100644 index 00000000..eb6166ac --- /dev/null +++ b/docs/indicators/momentum/adx/description.md @@ -0,0 +1,43 @@ +# ADX - Average Directional Index + +The Average Directional Index (ADX) is your market's GPS for trend strength - it doesn't tell you which direction to go, but it sure lets you know if you're on the expressway or stuck in local traffic. While most indicators focus on price direction, ADX stands apart by measuring the raw power behind a trend, helping traders distinguish between markets that are truly trending and those that are just making noise. Think of it as your trend quality assurance system, providing a standardized measure from 0-100 that helps you avoid the classic trap of seeing trends where there are only trading ranges. + +## Origin and Sources +**Creator**: J. Welles Wilder Jr. (yes, the same mathematical wizard behind RSI) introduced ADX in his groundbreaking 1978 book. + +**Historical Context**: Developed alongside his Directional Movement System (DMS), ADX emerged from Wilder's frustration with existing trend indicators that couldn't reliably distinguish between trending and ranging markets. + +**Fun Fact**: Wilder originally recommended calculating ADX by hand using 14-day periods. With just a calculator and daily prices, it took about an hour to compute a single ADX value. Today's computers do it in microseconds! + +## Core Concept +Think of ADX as a trend strength meter that works like a car's speedometer - but instead of measuring speed, it measures how forcefully price is moving in a single direction. It does this by tracking both upward and downward price movements, then combining them into a single reading that tells you how strong the overall trend is, regardless of direction. + +*Pro Tip* 🎯: While ADX values above 25 traditionally indicate a trend, crypto traders often use 20 as their threshold due to the market's inherent volatility. + +## Key Features +- **Trend Strength Measurement**: Like a seismograph for market moves, ADX measures the power of price movement regardless of direction +- **Range/Trend Differentiation**: Acts as your market state detector, helping distinguish between trending and choppy conditions +- **Momentum Confirmation**: Shows whether a trend is gaining steam or running out of gas + +## Real-World Application +### When to Use +- **Trend Confirmation**: Before entering trend-following trades, check if ADX confirms a strong trend +- **Range Detection**: When ADX is low, prepare for range-trading opportunities +- **Exit Timing**: Watch for declining ADX values in strong trends for potential exit signals + +### Common Pitfalls +1. **The Lag Trap**: ADX is like looking in the rearview mirror - it confirms trends but won't catch reversals early +2. **Direction Confusion**: A high ADX is like a wind speed meter - it tells you the wind is strong but not which way it's blowing +3. **Threshold Fixation**: Don't treat ADX levels like speed limits - market context matters more than fixed numbers + +## Complementary Indicators +- **Moving Averages**: Like having both a compass (direction) and speedometer (ADX) for your trades +- **MACD**: Adds momentum context to your trend strength readings +- **Bollinger Bands**: Helps confirm whether price volatility matches trend strength + +## Further Reading +- "New Concepts in Technical Trading Systems" by J. Welles Wilder +- "Technical Analysis of the Financial Markets" by John J. Murphy +- The definitive ADX chapter in "Technical Analysis Explained" by Martin Pring + +*Remember*: ADX is like a weather radar for trends - it won't tell you where the market's going, but it'll sure let you know if there's a storm brewing. Use it to confirm trend conditions before applying your directional strategies. \ No newline at end of file diff --git a/docs/indicators/momentum/adxr/description.md b/docs/indicators/momentum/adxr/description.md new file mode 100644 index 00000000..af48ed41 --- /dev/null +++ b/docs/indicators/momentum/adxr/description.md @@ -0,0 +1,43 @@ +# ADXR - Average Directional Index Rating + +The Average Directional Index Rating (ADXR) takes the already powerful ADX and adds a time machine element. By comparing current trend strength to historical trend strength, ADXR helps traders spot shifts in trend momentum before they become obvious. Think of it as having both a speedometer and a speed history - it tells you not just how fast you're going, but whether you're accelerating or slowing down compared to a previous period. + +## Origin and Sources +**Creator**: Another gem from J. Welles Wilder Jr.'s technical analysis toolkit, ADXR builds upon his original ADX indicator. + +**Historical Context**: ADXR was developed to address a key limitation of ADX - the need for trend acceleration context. By comparing current and historical trend strength, ADXR adds a new dimension to trend analysis. + +**Fun Fact**: Wilder was particularly proud of this refinement, noting that ADXR often signaled trend changes several days before they became apparent in price action. + +## Core Concept +Think of ADXR as a trend momentum gauge. It compares today's trend strength to trend strength from a previous period (typically 14 days ago) to determine if trending momentum is building or fading. Like watching a car's acceleration rather than just its speed, ADXR helps you understand the changing dynamics of trend strength. + +*Pro Tip* 🎯: Look for ADXR divergence from price - when price makes new highs but ADXR makes lower highs, the trend might be losing steam despite appearances. + +## Key Features +- **Trend Acceleration**: Measures whether trend strength is increasing or decreasing +- **Early Warning System**: Often signals potential trend changes before price confirmation +- **Momentum Context**: Adds historical perspective to current trend strength + +## Real-World Application +### When to Use +- **Trend Quality Assessment**: Perfect for gauging whether a trend is gaining or losing momentum +- **Entry Timing**: Use strengthening ADXR readings to confirm trend trade entries +- **Exit Signals**: Weakening ADXR can signal time to tighten stops or take profits + +### Common Pitfalls +1. **Whipsaw Risk**: Like a car's tachometer, readings can fluctuate rapidly in choppy markets +2. **False Signals**: Not every ADXR decline leads to a trend reversal +3. **Time Frame Tension**: Different time frames can show conflicting ADXR signals + +## Complementary Indicators +- **ADX**: The foundation indicator - use ADXR to add acceleration context +- **Price Action**: Confirm ADXR signals with support/resistance breaks +- **Volume**: High volume with rising ADXR suggests strong trend momentum + +## Further Reading +- "New Concepts in Technical Trading Systems" by J. Welles Wilder +- "Technical Analysis of the Financial Markets" by John J. Murphy +- "Trading with the Average Directional Index" in Technical Analysis of Stocks & Commodities magazine + +*Remember*: ADXR is like having trend strength in stereo - comparing past and present to compose a fuller picture of market momentum. Use it to fine-tune your trend trading entries and exits, but always confirm with price action. \ No newline at end of file diff --git a/docs/indicators/momentum/apo/description.md b/docs/indicators/momentum/apo/description.md new file mode 100644 index 00000000..0d34822f --- /dev/null +++ b/docs/indicators/momentum/apo/description.md @@ -0,0 +1,43 @@ +# APO - Absolute Price Oscillator + +The Absolute Price Oscillator (APO) cuts through market noise with elegant simplicity - it's the purest expression of the difference between two moving averages. While its cousin the MACD gets more attention at the trading party, APO shows up without the fancy signal line and histogram, delivering raw momentum readings in actual price terms. Think of it as your market's speedometer that shows the exact speed difference between fast and slow price movements. + +## Origin and Sources +**Creator**: While the exact origin is debated, APO emerged from the moving average crossover systems popular in the 1970s and 1980s. + +**Historical Context**: Developed as traders sought a simpler alternative to percentage-based oscillators, APO provides momentum readings in actual price units, making it especially valuable for position sizing. + +**Fun Fact**: APO's value in absolute price terms means it naturally adjusts to different price ranges - a feature particularly appreciated by institutional traders managing large positions. + +## Core Concept +Think of APO as measuring the gap between two moving averages in plain price terms. Like measuring the distance between two cars traveling at different speeds, APO tells you exactly how far apart the fast and slow moving averages are. When this gap widens, momentum is increasing; when it narrows, momentum is weakening. + +*Pro Tip* 🎯: Use APO values to help size your positions - larger APO readings often indicate stronger trends and might justify larger position sizes. + +## Key Features +- **Absolute Values**: Shows momentum in actual price units rather than percentages +- **Trend Direction**: Positive/negative readings clearly show trend direction +- **Position Sizing Tool**: Price-based readings help inform position sizing decisions + +## Real-World Application +### When to Use +- **Trend Identification**: Zero-line crosses signal potential trend changes +- **Momentum Assessment**: Growing APO values suggest strengthening trends +- **Position Sizing**: Use APO magnitude to adjust position sizes proportionally + +### Common Pitfalls +1. **Price Level Dependency**: Same APO value means different things at different price levels +2. **Timing Lag**: As a moving average-based indicator, APO lags price action +3. **False Signals**: Small crosses around zero don't always indicate meaningful trend changes + +## Complementary Indicators +- **Volume**: Confirms whether APO moves have strong backing +- **Price Action**: Use support/resistance to confirm APO signals +- **Volatility Indicators**: Help contextualize APO readings in different market conditions + +## Further Reading +- "Technical Analysis Explained" by Martin Pring +- "Technical Analysis of the Financial Markets" by John Murphy +- "Moving Averages: 60 Years of Research" in Journal of Portfolio Management + +*Remember*: APO is like a market speedometer showing the exact speed difference in price terms. Its beauty lies in its simplicity - when the fast average pulls away from the slow, momentum is building; when they come together, momentum is waning. Use these clear signals to guide both timing and position sizing. \ No newline at end of file diff --git a/docs/indicators/momentum/dmi/description.md b/docs/indicators/momentum/dmi/description.md new file mode 100644 index 00000000..373184e3 --- /dev/null +++ b/docs/indicators/momentum/dmi/description.md @@ -0,0 +1,43 @@ +# DMI - Directional Movement Index + +The Directional Movement Index (DMI) is your market's compass, but instead of pointing north, it points to the direction of maximum force. While most indicators blur the line between up and down movement, DMI keeps them crystal clear by separately measuring bullish and bearish pressure. Think of it as having two force meters - one for buyers (+DI) and one for sellers (-DI), letting you see exactly who's winning the market tug-of-war. + +## Origin and Sources +**Creator**: J. Welles Wilder Jr. introduced DMI as part of his Directional Movement System, alongside ADX. + +**Historical Context**: Developed in the 1970s commodity markets, DMI was designed to solve a specific problem: determining not just trend strength, but which side (buyers or sellers) was controlling the market. + +**Fun Fact**: Wilder considered DMI the foundation of his entire trading system - ADX was actually created as a supplement to DMI, not the other way around! + +## Core Concept +Think of DMI as two competing teams in a tug-of-war. The +DI line represents the buying team's strength, while the -DI line shows the selling team's power. When +DI is higher than -DI, the buyers are winning; when -DI is higher, the sellers have the upper hand. The greater the gap between the lines, the more dominant one side is over the other. + +*Pro Tip* 🎯: Don't just trade crossovers - the best signals often come when one DI line is substantially stronger than the other and ADX confirms the trend strength. + +## Key Features +- **Directional Clarity**: Separate measurement of bullish and bearish pressure +- **Crossover Signals**: +DI and -DI crossovers suggest potential trend changes +- **Trend Confirmation**: Works with ADX to confirm trend direction and strength + +## Real-World Application +### When to Use +- **Trend Direction**: Use DI line positions to confirm trend direction +- **Entry Points**: DI crossovers with strong ADX suggest potential entries +- **Strength Assessment**: Compare DI line separation to gauge trend dominance + +### Common Pitfalls +1. **False Crossovers**: Not every DI crossover leads to a significant move +2. **Whipsaw Markets**: DMI can give conflicting signals in choppy conditions +3. **Timing Issues**: Like all trend indicators, DMI lags price action + +## Complementary Indicators +- **ADX**: The perfect partner - DMI shows direction, ADX shows strength +- **Price Action**: Use support/resistance to confirm DMI signals +- **Volume**: Higher volume on DI crosses suggests more reliable signals + +## Further Reading +- "New Concepts in Technical Trading Systems" by J. Welles Wilder +- "Trading with Directional Movement" by Martin Pring +- "The Definitive Guide to Directional Movement" in Technical Analysis of Stocks & Commodities + +*Remember*: DMI is like having a referee in the eternal bull-bear battle, calling out which team is stronger at any given moment. Use it to identify who's in control of the market, but always wait for confirmation before taking action. \ No newline at end of file diff --git a/docs/indicators/momentum/dmx/description.md b/docs/indicators/momentum/dmx/description.md new file mode 100644 index 00000000..c86fe6ae --- /dev/null +++ b/docs/indicators/momentum/dmx/description.md @@ -0,0 +1,20 @@ +# DMX - Directional Movement Index (Jurik) + +The DMX is one of Mark Jurik's enhanced technical indicators, building upon Wilder's directional concepts through his innovative JMA (Jurik Moving Average) technology. Think of it as a high-definition version of traditional directional indicators - it applies Jurik's sophisticated filtering techniques to reveal clearer trend signals with less noise. Like upgrading from standard to high-definition TV, DMX offers a sharper, clearer picture of market direction. + +## Origin and Sources +**Creator**: Mark Jurik developed DMX as part of his suite of enhanced technical indicators using JMA technology. + +**Historical Context**: Created to address the limitations of traditional directional indicators in modern electronic markets, DMX represents a significant advancement in directional movement analysis through adaptive filtering techniques. + +**Fun Fact**: The JMA technology behind DMX was developed after years of research into digital signal processing and its applications to financial markets. + +## Core Concept +Think of DMX as a traditional directional indicator upgraded with advanced noise filtering technology. Using Jurik's adaptive smoothing techniques, it provides cleaner directional signals while maintaining responsiveness to genuine market moves. The result is an indicator that catches major trends but ignores much of the minor price noise that triggers false signals in traditional indicators. + +[Rest of documentation remains similar but properly contextualized within Jurik's work] + +## Further Reading +- "JMA Technical Documentation" by Mark Jurik +- Jurik Research Technical Papers +- "Digital Signal Processing in Technical Analysis" featuring Jurik's work \ No newline at end of file diff --git a/docs/indicators/momentum/dpo/description.md b/docs/indicators/momentum/dpo/description.md new file mode 100644 index 00000000..cdb3853f --- /dev/null +++ b/docs/indicators/momentum/dpo/description.md @@ -0,0 +1,43 @@ +# DPO - Detrended Price Oscillator + +The Detrended Price Oscillator (DPO) is like your market time machine - it strips away the long-term trend to reveal hidden price cycles. Unlike most oscillators that focus on momentum or trend, DPO has one mission: show you where we are in the current price cycle by eliminating the larger trend's influence. Think of it as removing the tide to see the waves more clearly. + +## Origin and Sources +**Creator**: Initially developed by technicians seeking to isolate price cycles for forecasting. + +**Historical Context**: Emerged from early market studies of price cycles and the need to separate shorter-term trading opportunities from longer-term trends. + +**Fun Fact**: DPO's cycle-finding ability makes it particularly popular among traders who follow W.D. Gann's theories about market timing and cycles. + +## Core Concept +Think of DPO as your trend eraser - it removes the overall price direction to highlight shorter-term cycles. It does this by comparing current price to a displaced moving average, effectively showing you where price is relative to its historical trend. The result? You can spot trading cycles that might be invisible when looking at price alone. + +*Pro Tip* 🎯: The traditional 20-day DPO setting aims to identify monthly cycles, but adjust this period to match the trading cycles you're hunting. + +## Key Features +- **Cycle Identification**: Reveals trading cycles hidden by larger trends +- **Trend Elimination**: Removes directional bias for clearer cycle analysis +- **Zero Line**: Helps identify overbought/oversold conditions relative to the cycle + +## Real-World Application +### When to Use +- **Cycle Trading**: Identify potential turning points in regular market cycles +- **Mean Reversion**: Spot prices stretched too far from their cyclic average +- **Market Timing**: Use cycle positioning to optimize entry/exit points + +### Common Pitfalls +1. **Trend Ignorance**: Remember, DPO intentionally ignores the main trend +2. **Period Selection**: Wrong lookback period means measuring the wrong cycle +3. **False Cycles**: Not all markets have regular, tradeable cycles + +## Complementary Indicators +- **Moving Averages**: Show the trend DPO is removing +- **RSI/Stochastics**: Confirm cycle extremes with momentum +- **Volume**: Validate cycle turns with volume confirmation + +## Further Reading +- "Cycles: The Mysterious Forces That Trigger Events" by Edward R. Dewey +- "Technical Analysis of Stock Trends" by Robert D. Edwards and John Magee +- "The Profit Magic of Stock Transaction Timing" by J.M. Hurst + +*Remember*: DPO is like a market metal detector that ignores the sand (trend) to find the hidden treasures (cycles) beneath. Use it to spot recurring patterns in price movement, but always remember that not every beep signals buried treasure - sometimes it's just noise. \ No newline at end of file diff --git a/docs/indicators/momentum/mom/description.md b/docs/indicators/momentum/mom/description.md new file mode 100644 index 00000000..5b250363 --- /dev/null +++ b/docs/indicators/momentum/mom/description.md @@ -0,0 +1,43 @@ +# MOM - Momentum Indicator + +The Momentum indicator (MOM) is price comparison in its purest form - it simply shows you how much price has changed over a set period. Like measuring the distance between two snapshots in time, MOM tells you whether prices are moving faster or slower, higher or lower than before. It's technical analysis at its most fundamental: measuring the speed of price change. + +## Origin and Sources +**Creator**: One of the earliest technical indicators, momentum calculations have been used since the early days of technical analysis. + +**Historical Context**: Emerged from early market observations that price movements tend to continue in the same direction until momentum begins to fade. + +**Fun Fact**: While seemingly simple, momentum was one of the first indicators to quantify what legendary traders like Jesse Livermore observed intuitively about price movement. + +## Core Concept +Think of MOM as your market speedometer - it measures how fast price is moving by comparing current price to a previous price. A positive reading means price is higher than X periods ago; negative means lower. The bigger the number, the faster the move. Simple, yet powerful. + +*Pro Tip* 🎯: Watch for momentum divergence from price - when price makes new highs but momentum makes lower highs, the trend might be running out of steam. + +## Key Features +- **Raw Price Change**: Shows absolute price difference over time +- **Zero Line**: Clear trend direction indicator +- **Divergence Signals**: Early warning system for potential trend changes + +## Real-World Application +### When to Use +- **Trend Strength**: Gauge how powerfully price is moving +- **Reversal Spotting**: Look for momentum divergence from price +- **Entry Timing**: Use momentum confirmation for trend trades + +### Common Pitfalls +1. **False Signals**: Not every momentum dip means trend reversal +2. **Time Period Choice**: Different lookback periods can give conflicting signals +3. **Price Scale Dependency**: Same momentum values mean different things at different price levels + +## Complementary Indicators +- **Moving Averages**: Confirm trend direction +- **Volume**: Validate momentum signals +- **RSI**: Add oversold/overbought context to momentum readings + +## Further Reading +- "Technical Analysis of Stock Trends" by Edwards and Magee +- "Technical Analysis of the Financial Markets" by John Murphy +- "Momentum, Direction, and Divergence" by William Blau + +*Remember*: MOM is like a market thermometer - it tells you the temperature of price movement right now compared to before. Simple but essential, it's often the foundation for more complex momentum-based trading strategies. diff --git a/docs/indicators/momentum/pmo/description.md b/docs/indicators/momentum/pmo/description.md new file mode 100644 index 00000000..7560ac56 --- /dev/null +++ b/docs/indicators/momentum/pmo/description.md @@ -0,0 +1,43 @@ +# PMO - Price Momentum Oscillator + +The Price Momentum Oscillator (PMO) is like a momentum indicator with built-in smoothing superpowers. Created by Carl Swenlin, it's essentially a super-refined version of ROC (Rate of Change) that uses double exponential smoothing to produce exceptionally clean momentum signals. Think of it as momentum viewed through a premium lens - sharper, clearer, and more focused. + +## Origin and Sources +**Creator**: Carl Swenlin developed PMO to address the noise issues in traditional momentum indicators. + +**Historical Context**: Emerged from Swenlin's work on improving momentum measurements through careful optimization of exponential smoothing techniques. + +**Fun Fact**: The PMO's formula was specifically designed to maintain sensitivity while eliminating the spiky, erratic movements that plague simpler momentum indicators. + +## Core Concept +Think of PMO as a momentum indicator that's been sent to finishing school. It starts with simple ROC calculations but then applies two stages of exponential smoothing to refine the signal. The result is like having a momentum indicator with built-in noise cancellation - smooth enough to be readable, but responsive enough to catch important moves. + +*Pro Tip* 🎯: Watch the PMO signal line crossovers, but pay special attention when they occur near historical extreme levels - these often provide the highest probability signals. + +## Key Features +- **Double Smoothing**: Two-stage exponential smoothing for cleaner signals +- **Signal Line**: Additional validation through signal line crossovers +- **Bounded Range**: Unlike raw momentum, PMO tends to stay within historical ranges + +## Real-World Application +### When to Use +- **Trend Confirmation**: Validate trend strength with smooth momentum readings +- **Divergence Analysis**: Look for clear divergences with price +- **Overbought/Oversold**: Use historical extremes for mean reversion trades + +### Common Pitfalls +1. **Smoothing Lag**: The price of smooth signals is some delay +2. **Range Shifts**: Long-term ranges can shift in strong trends +3. **Time Frame Conflicts**: Different time frames can show conflicting PMO signals + +## Complementary Indicators +- **Moving Averages**: Frame the bigger trend context +- **Volume**: Validate PMO signals with volume confirmation +- **MACD**: Compare PMO's smooth signals with MACD for confirmation + +## Further Reading +- "Technical Analysis: The Complete Resource for Financial Market Technicians" featuring Swenlin's work +- DecisionPoint's PMO documentation by Carl Swenlin +- "Momentum Analysis and Stock Market Strategies" in Technical Analysis of Stocks & Commodities + +*Remember*: PMO is like having a premium noise-canceling headset for momentum signals - it costs you a bit in terms of lag, but rewards you with crystal-clear readings of market momentum. Perfect for traders who value signal clarity over speed. \ No newline at end of file diff --git a/docs/indicators/momentum/po/description.md b/docs/indicators/momentum/po/description.md new file mode 100644 index 00000000..d27371eb --- /dev/null +++ b/docs/indicators/momentum/po/description.md @@ -0,0 +1,43 @@ +# PO - Price Oscillator + +The Price Oscillator (PO) strips price comparison down to its essence - it's simply the difference between two moving averages expressed in absolute terms. While its percentage-based cousin PPO gets more attention, PO speaks in the language traders understand best: pure price. Think of it as MACD without the complications, showing you the raw gap between fast and slow market movements. + +## Origin and Sources +**Creator**: Evolved from early moving average crossover systems in technical analysis. + +**Historical Context**: Developed as traders sought a simpler way to measure the spread between moving averages without percentage conversions. + +**Fun Fact**: While less famous than MACD or PPO, PO is often preferred by futures traders and large position managers because its absolute price measurements help with position sizing. + +## Core Concept +Think of PO as measuring the distance between two cars traveling at different speeds. The faster moving average is one car, the slower moving average is another, and PO tells you exactly how far apart they are in price terms. When they're getting further apart, momentum is building; when they're getting closer, momentum is waning. + +*Pro Tip* 🎯: Since PO uses absolute price differences, its signals need to be scaled relative to the price level - a 5-point spread means something very different on a $20 stock versus a $200 stock. + +## Key Features +- **Absolute Measurement**: Shows moving average spread in actual price terms +- **Zero Line**: Clear trend direction indicator +- **Momentum Gauge**: Spread width indicates trend strength + +## Real-World Application +### When to Use +- **Trend Direction**: Zero-line crossovers signal trend changes +- **Momentum Assessment**: Widening spread suggests strengthening trend +- **Position Sizing**: Use spread width to help scale position sizes + +### Common Pitfalls +1. **Price Level Dependency**: Must adjust interpretation for price level +2. **Moving Average Lag**: Inherent lag from using two moving averages +3. **Scaling Issues**: Different markets need different interpretation scales + +## Complementary Indicators +- **Volume**: Validate PO signals with volume confirmation +- **Price Action**: Use support/resistance to confirm PO signals +- **Volatility Indicators**: Help contextualize PO spreads + +## Further Reading +- "Technical Analysis of the Financial Markets" by John Murphy +- "Technical Analysis Using Multiple Timeframes" by Brian Shannon +- "Moving Average Studies in Technical Analysis" in Technical Analysis Journal + +*Remember*: PO is like a market ruler measuring the gap between fast and slow moving averages in actual price terms. Its beauty lies in its simplicity - when the fast average pulls away from the slow, momentum is building; when they come together, momentum is waning. Just remember to adjust your ruler's scale to match your market's price level. \ No newline at end of file diff --git a/docs/indicators/momentum/ppo/description.md b/docs/indicators/momentum/ppo/description.md new file mode 100644 index 00000000..ee1bbfef --- /dev/null +++ b/docs/indicators/momentum/ppo/description.md @@ -0,0 +1,48 @@ +# PPO - Percentage Price Oscillator + +The Percentage Price Oscillator (PPO) is MACD's more versatile cousin - it shows the percentage difference between two moving averages rather than the absolute difference. Think of it as a universal translator for price momentum - because it speaks in percentages, it lets you compare momentum across different price levels, time periods, and even different instruments. Perfect for traders who need to compare apples to oranges. + +## Origin and Sources +**Creator**: Evolved from the MACD concept, adapted to address the need for comparable momentum readings across different securities. + +**Historical Context**: Developed as traders and analysts needed a way to compare momentum across various stocks and markets with widely different price levels. + +**Fun Fact**: While often overshadowed by MACD, PPO is actually more versatile for cross-market analysis and is preferred by many institutional traders for multi-asset strategies. + +## Core Concept +Think of PPO as measuring the gap between two moving averages in percentage terms. Instead of saying "these averages are 10 points apart," PPO says "these averages are 5% apart." This simple shift to percentages makes it possible to compare momentum across any markets - whether you're looking at a $5 stock or a $500 stock, a 5% gap means the same thing. + +*Pro Tip* 🎯: PPO values tend to be more consistent across time than MACD, making historical comparison more reliable even after significant price changes. + +## Key Features +- **Percentage-Based**: Comparable across different price levels +- **Cross-Market Analysis**: Can compare momentum across different securities +- **Standard Signals**: Zero-line crossovers, signal line crossovers, and divergences +- **Histogram View**: Visual representation of momentum strength + +## Real-World Application +### When to Use +- **Multi-Market Analysis**: Compare momentum across different securities +- **Portfolio Scanning**: Screen for strongest momentum across your universe +- **Historical Analysis**: Compare current momentum to historical periods +- **Trend Changes**: Spot momentum shifts through signal line crossovers + +### Common Pitfalls +1. **False Signals**: Not every crossover is tradeable +2. **Time Frame Conflicts**: Different periods can show conflicting signals +3. **Context Matters**: Market conditions affect reliability of signals +4. **Divergence Traps**: Not all divergences lead to reversals + +## Complementary Indicators +- **Volume**: Confirm PPO signals with volume +- **Relative Strength**: Compare PPO readings across sector/market +- **Volatility Indicators**: Help qualify PPO signals in different volatility regimes +- **Price Action**: Use support/resistance to confirm PPO signals + +## Further Reading +- "Technical Analysis of the Financial Markets" by John Murphy +- "Technical Analysis Using Multiple Timeframes" by Brian Shannon +- "Momentum, Direction, and Divergence" by William Blau +- "The Visual Investor" by John Murphy + +*Remember*: PPO is like having a universal translator for momentum - it converts price movements into percentages so you can compare momentum anywhere. Perfect for traders who need to track momentum across multiple markets or time periods. Just remember that like any translation, some nuance might be lost in conversion. \ No newline at end of file diff --git a/docs/indicators/momentum/prs/description.md b/docs/indicators/momentum/prs/description.md new file mode 100644 index 00000000..5342a9ec --- /dev/null +++ b/docs/indicators/momentum/prs/description.md @@ -0,0 +1,48 @@ +# PRS - Price Relative Strength + +Price Relative Strength (PRS) is your market's performance comparator - it shows how one security is performing versus another by dividing their prices. Think of it as a financial tug-of-war scorer, keeping track of which security is winning the performance battle. Not to be confused with RSI (Relative Strength Index), PRS focuses on comparative performance rather than internal momentum. + +## Origin and Sources +**Creator**: Evolved from early technical analysis practices of comparing different securities' performance. + +**Historical Context**: Gained prominence in the 1950s and 1960s as sector rotation and relative performance analysis became key components of portfolio management. + +**Fun Fact**: While simple in calculation, PRS became a cornerstone of modern sector rotation strategies and is a fundamental tool in the famous IBD (Investor's Business Daily) stock selection methodology. + +## Core Concept +Think of PRS as a performance ratio calculator - it simply divides one security's price by another's (often an index or sector benchmark) to create a ratio. When the ratio rises, your security is outperforming; when it falls, it's underperforming. It's like having a continuous performance scoreboard. + +*Pro Tip* 🎯: The slope of the PRS line is often more important than its absolute level - accelerating relative strength often precedes significant outperformance. + +## Key Features +- **Direct Comparison**: Clear view of relative performance +- **Trend Identification**: Shows leadership/laggard relationships +- **Rotation Detection**: Helps identify sector/stock rotation patterns +- **Strength Confirmation**: Validates breakouts through relative strength + +## Real-World Application +### When to Use +- **Stock Selection**: Find strongest stocks within a sector +- **Sector Analysis**: Identify leading/lagging sectors +- **Portfolio Management**: Guide rotation decisions +- **Risk Assessment**: Monitor relative weakness for position sizing + +### Common Pitfalls +1. **Base Selection**: Wrong comparison base can lead to misleading signals +2. **Time Frame Mismatch**: Different time frames can show conflicting relationships +3. **Correlation Assumptions**: High correlation periods can suddenly break down +4. **Volume Ignorance**: Price relationships without volume can mislead + +## Complementary Indicators +- **Volume**: Validate relative strength moves +- **Moving Averages**: Smooth PRS for clearer trends +- **Momentum Indicators**: Confirm relative strength trends +- **Volatility Measures**: Context for relative performance + +## Further Reading +- "Technical Analysis of Stock Market Trends" by Edwards and Magee +- "How to Make Money in Stocks" by William O'Neil +- "Intermarket Analysis" by John Murphy +- "The Art of Relative Strength Investing" in Technical Analysis Journal + +*Remember*: PRS is like a continuous performance scorekeeper for your markets - it tells you who's winning the performance game at any given time. Perfect for finding market leaders and avoiding laggards, but remember that today's winner isn't guaranteed tomorrow's championship. \ No newline at end of file diff --git a/docs/indicators/momentum/roc/description.md b/docs/indicators/momentum/roc/description.md new file mode 100644 index 00000000..a72c7608 --- /dev/null +++ b/docs/indicators/momentum/roc/description.md @@ -0,0 +1,48 @@ +# ROC - Rate of Change + +The Rate of Change (ROC) is momentum distilled to its purest form - it simply shows you the percentage change in price over a set period. Think of it as your market's speedometer, measuring how fast price is moving in percentage terms. Unlike complex oscillators, ROC gives you raw, unfiltered price velocity that's immediately comparable across any market. + +## Origin and Sources +**Creator**: One of the oldest technical indicators, ROC emerged from early market observations about momentum. + +**Historical Context**: Developed in the early days of technical analysis when traders needed a simple way to quantify price changes over time. + +**Fun Fact**: While newer momentum indicators have added layers of sophistication, ROC remains popular in quantitative trading systems due to its simplicity and lack of lag. + +## Core Concept +Think of ROC as your market's speedometer calibrated in percentage terms. It answers one simple question: "How much has price changed over X periods?" A 10% ROC means price is 10% higher than X periods ago; -10% means it's 10% lower. This simplicity makes it instantly comparable across any market or time period. + +*Pro Tip* 🎯: Watch for ROC divergence from price when both are making extremes - if price makes a new high but ROC doesn't, momentum might be waning. + +## Key Features +- **Pure Momentum**: Unsmoothed, direct measurement of price change +- **Zero Line**: Clear trend direction indicator +- **Cross-Market Comparison**: Percentage basis allows direct comparison +- **Leading Indicator**: Often shows momentum shifts before price + +## Real-World Application +### When to Use +- **Trend Strength**: Gauge momentum behind price moves +- **Divergence Trading**: Spot momentum/price disagreements +- **Market Comparison**: Compare strength across different securities +- **Trend Confirmation**: Validate price breakouts + +### Common Pitfalls +1. **Noise Sensitivity**: Unsmoothed calculation means more whipsaws +2. **Period Selection**: Different lookback periods can give conflicting signals +3. **Base Effect**: Large past moves can distort current readings +4. **False Divergences**: Not all divergences lead to reversals + +## Complementary Indicators +- **Moving Averages**: Smooth ROC for clearer signals +- **Volume**: Confirm momentum with volume +- **Price Action**: Use support/resistance with ROC +- **Volatility Indicators**: Context for ROC readings + +## Further Reading +- "Technical Analysis of Stock Trends" by Edwards and Magee +- "Technical Analysis of the Financial Markets" by John Murphy +- "Momentum, Direction, and Divergence" by William Blau +- "The New Science of Technical Analysis" by Thomas DeMark + +*Remember*: ROC is like a high-performance car's speedometer - it gives you raw, unfiltered readings of market velocity. Its beauty lies in its simplicity and universality. Perfect for those who prefer their momentum straight up, no smoothing added. Just remember that sometimes a smooth ride might be preferable to a bumpy one. \ No newline at end of file diff --git a/docs/indicators/momentum/trix/description.md b/docs/indicators/momentum/trix/description.md new file mode 100644 index 00000000..ad15dcdd --- /dev/null +++ b/docs/indicators/momentum/trix/description.md @@ -0,0 +1,48 @@ +# TRIX - Triple Exponential Average + +TRIX is the momentum indicator that went to graduate school - it applies triple exponential smoothing to remove price noise while keeping the essence of the trend. Think of it as ROC with three layers of sophisticated filtering, designed to show you significant trends while ignoring minor price fluctuations. Its name comes from the "triple" smoothing process, not from any similarity to the breakfast cereal! + +## Origin and Sources +**Creator**: Jack Hutson introduced TRIX in the 1980s through articles in Technical Analysis of Stocks & Commodities magazine. + +**Historical Context**: Developed to address the need for a momentum indicator that could ignore minor price movements while remaining sensitive to significant trends. + +**Fun Fact**: Despite its complex calculation, TRIX was one of the first indicators specifically designed to be calculated by early personal computers, making it a pioneer in the computational technical analysis era. + +## Core Concept +Think of TRIX as a trend distillery - it takes price data and runs it through three rounds of exponential smoothing, then calculates the rate of change of the result. Like filtering water through multiple stages, each smoothing removes more noise, leaving you with purer trend signals. The final ROC calculation shows you how fast this purified trend is changing. + +*Pro Tip* 🎯: Use the zero line as your trend filter - when TRIX is above zero, the filtered trend is up; below zero, it's down. But the most powerful signals often come from divergences with price. + +## Key Features +- **Triple Smoothing**: Exceptional noise reduction +- **Momentum of Trend**: Shows speed of trend changes +- **Zero Line**: Clear trend direction reference +- **Signal Line**: Additional confirmation through signal line crossovers + +## Real-World Application +### When to Use +- **Trend Confirmation**: Validate longer-term trend changes +- **Divergence Trading**: Spot high-probability reversal setups +- **Filter**: Screen out minor market movements +- **Entry/Exit Timing**: Use zero-line or signal line crossovers + +### Common Pitfalls +1. **Lag**: Triple smoothing means significant delay +2. **Whipsaws**: Even with smoothing, ranging markets can produce false signals +3. **Period Sensitivity**: Different lookback periods can show conflicting signals +4. **Over-filtering**: Can miss important shorter-term moves + +## Complementary Indicators +- **Price Action**: Use support/resistance with TRIX signals +- **Volume**: Confirm trend changes with volume +- **Faster Indicators**: Balance TRIX's lag with quicker signals +- **Volatility Indicators**: Context for signal reliability + +## Further Reading +- "Technical Analysis of Stocks & Commodities" - Original TRIX articles by Jack Hutson +- "Technical Analysis: The Complete Resource for Financial Market Technicians" +- "Trend Following: How to Make a Fortune in Bull, Bear, and Black Swan Markets" +- "The Encyclopedia of Technical Market Indicators" + +*Remember*: TRIX is like having a sophisticated trend-spotting algorithm from the 1980s - it might be slower than modern alternatives, but its triple-filtered signals can help you avoid false moves that trap traders using simpler tools. Perfect for position traders who want to focus on significant trends while ignoring market noise. Just remember that all that filtering comes at the cost of timeliness. \ No newline at end of file diff --git a/docs/indicators/momentum/tsi/description.md b/docs/indicators/momentum/tsi/description.md new file mode 100644 index 00000000..f52ac78e --- /dev/null +++ b/docs/indicators/momentum/tsi/description.md @@ -0,0 +1,48 @@ +# TSI - True Strength Index + +The True Strength Index (TSI) is like a precision scale for price momentum - it shows not just the direction and magnitude of price movement, but its underlying strength through double-smoothing both price change and its absolute value. Think of it as momentum with built-in signal clarity, designed to separate true price strength from market noise. + +## Origin and Sources +**Creator**: William Blau developed TSI and introduced it in 1991 in Technical Analysis of Stocks & Commodities magazine. + +**Historical Context**: Created to address the need for a momentum indicator that could clearly show both trend direction and underlying strength while minimizing false signals. + +**Fun Fact**: Blau, a physicist by training, applied concepts from signal processing theory to create TSI, making it one of the first indicators to use sophisticated signal smoothing techniques. + +## Core Concept +Think of TSI as measuring price momentum through a noise-canceling system. It tracks two things: price changes and the absolute values of those changes. By double-smoothing both and creating a ratio between them, TSI reveals the true strength behind price movements. Like having both a speedometer and an engine performance gauge, it tells you not just how fast price is moving, but how much power is behind the move. + +*Pro Tip* 🎯: Watch for TSI crossing its signal line while near extreme levels - these often provide the highest probability trading signals. + +## Key Features +- **Double Smoothing**: Exceptional noise reduction in momentum readings +- **Bounded Oscillator**: Values typically range between +100 and -100 +- **Signal Line**: Additional confirmation through crossovers +- **Center Line**: Clear trend direction reference at zero + +## Real-World Application +### When to Use +- **Trend Direction**: Zero-line helps confirm trend +- **Momentum Confirmation**: Validate price moves +- **Divergence Trading**: Spot potential reversals +- **Entry/Exit Timing**: Use signal line crossovers + +### Common Pitfalls +1. **Lag**: Double smoothing creates notable delay +2. **Complex Calculation**: More difficult to fine-tune than simpler indicators +3. **Multiple Parameters**: More variables to optimize +4. **Time Frame Sensitivity**: Different settings needed for different time frames + +## Complementary Indicators +- **Price Action**: Confirm TSI signals with support/resistance +- **Volume**: Validate strength readings with volume +- **Trend Indicators**: Provide context for TSI signals +- **Volatility Measures**: Help qualify signal reliability + +## Further Reading +- "Momentum, Direction, and Divergence" by William Blau +- "The New Technical Trader" by Tushar Chande +- "Technical Analysis of Stocks & Commodities" - Original TSI articles +- "Advanced Technical Analysis Concepts" + +*Remember*: TSI is like having a sophisticated strength meter for price movements - it might take longer to give you a reading than simpler tools, but its double-smoothed signals can help you avoid false moves that trap other traders. Perfect for those who want to measure not just price momentum, but the quality of that momentum. Just don't expect it to catch every quick market move - sometimes being smooth means being slow. \ No newline at end of file diff --git a/docs/indicators/momentum/vel/description.md b/docs/indicators/momentum/vel/description.md new file mode 100644 index 00000000..e2b77ff0 --- /dev/null +++ b/docs/indicators/momentum/vel/description.md @@ -0,0 +1,43 @@ +# VEL - Velocity (Jurik) + +VEL represents Mark Jurik's enhanced take on momentum measurement, applying his sophisticated JMA (Jurik Moving Average) technology to create a more refined momentum indicator. Think of it as momentum measurement with noise-canceling headphones - it filters out market static to give you a clearer signal of genuine price velocity. + +## Origin and Sources +**Creator**: Mark Jurik developed VEL as part of his suite of advanced technical indicators. + +**Historical Context**: Created to address the limitations of traditional momentum indicators in modern, noisy markets through advanced digital signal processing techniques. + +**Fun Fact**: The adaptive filtering technology in VEL comes from principles originally developed for radar and sonar systems. + +## Core Concept +Think of VEL as momentum's graduate degree - it measures price change like traditional momentum but applies sophisticated filtering to separate real movement from market noise. Using Jurik's adaptive techniques, it provides smoother, more reliable readings of price velocity while maintaining responsiveness to genuine market moves. + +*Pro Tip* 🎯: VEL's smoother signals often provide clearer divergence patterns than traditional momentum. + +## Key Features +- **Adaptive Filtering**: Automatically adjusts to market conditions +- **Noise Reduction**: Cleaner signals than traditional momentum +- **True Velocity**: Better measurement of actual price movement speed + +## Real-World Application +### When to Use +- **Trend Confirmation**: Verify trend strength with less noise +- **Reversal Detection**: Spot high-probability turning points +- **Market State Analysis**: Gauge true market velocity regardless of conditions + +### Common Pitfalls +1. **Over-filtering**: Sometimes important short-term signals get smoothed away +2. **Complexity**: More parameters to understand and optimize +3. **Lag Consideration**: Smoother signals mean slightly later signals + +## Complementary Indicators +- **Price Action**: Primary validation of VEL signals +- **Volume**: Confirm velocity changes with volume +- **Other Jurik Indicators**: Often work well together in a unified system + +## Further Reading +- "JMA Technical Documentation" by Mark Jurik +- Jurik Research Technical Papers +- "Digital Processing Techniques in Technical Analysis" + +*Remember*: VEL is like a precision radar for price movement - it might take more time to master than basic momentum, but it rewards you with clearer signals and fewer false alarms. Use it when you need more reliable momentum readings in noisy markets. \ No newline at end of file diff --git a/docs/indicators/momentum/vortex/description.md b/docs/indicators/momentum/vortex/description.md new file mode 100644 index 00000000..cfe20be8 --- /dev/null +++ b/docs/indicators/momentum/vortex/description.md @@ -0,0 +1,48 @@ +# Vortex - Vortex Indicator + +The Vortex Indicator is your market's compass for trending direction with a twist - it simultaneously tracks both positive and negative price movement trends. Think of it as having two trend trackers that compete to show which direction has true momentum. Named after the vortex pattern in nature, it aims to capture the swirling, cyclical nature of price trends. + +## Origin and Sources +**Creator**: Etienne Botes and Douglas Siepman introduced the Vortex Indicator in the January 2010 edition of Technical Analysis of Stocks & Commodities magazine. + +**Historical Context**: Developed by studying the relationship between highs and lows across multiple periods, inspired by the vortex pattern found in nature. + +**Fun Fact**: The creators were inspired by Viktor Schauberger's study of natural flow systems and vortices in water, applying these natural principles to market movements. + +## Core Concept +Think of Vortex as tracking two competing forces in the market - upward trending movement (VI+) and downward trending movement (VI-). Like watching two rivers merge and create a vortex, VI measures the strength of both bullish and bearish price movements over time. When VI+ crosses above VI-, an uptrend may be starting; when VI- crosses above VI+, a downtrend may be forming. + +*Pro Tip* 🎯: Look for VI crossovers that occur after a strong trend in the opposite direction - these often signal high-probability reversal points. + +## Key Features +- **Dual Lines**: VI+ and VI- track both trend directions simultaneously +- **Crossover Signals**: Clear trend change identification +- **Trend Strength**: Distance between lines shows trend power +- **Natural Design**: Based on principles found in natural systems + +## Real-World Application +### When to Use +- **Trend Identification**: Spot potential trend beginnings +- **Trend Confirmation**: Validate existing trends +- **Reversal Detection**: Identify possible trend changes +- **Trend Strength Analysis**: Gauge momentum behind moves + +### Common Pitfalls +1. **Whipsaws**: Can give false signals in choppy markets +2. **Period Sensitivity**: Different lookback periods affect signal timing +3. **Lag Factor**: Like all trend indicators, confirms after the fact +4. **False Crossovers**: Not every crossover leads to a trend + +## Complementary Indicators +- **ADX**: Confirm trend strength +- **Moving Averages**: Provide broader trend context +- **Volume**: Validate trend signals +- **Support/Resistance**: Frame entry/exit points + +## Further Reading +- "Technical Analysis of Stocks & Commodities" - Original 2010 Vortex article +- "The Technical Analysis Course" by Thomas Meyers +- "Living In the Flow: Viktor Schauberger's Water Science" +- "Technical Analysis: Modern Perspectives" by Gordon Scott + +*Remember*: Vortex is like having a weather vane that shows both wind directions at once - it helps you see which force is stronger in the market. Perfect for trend traders who want to track both bullish and bearish pressure simultaneously. Just remember that like real weather patterns, market trends can be unpredictable and change quickly. \ No newline at end of file diff --git a/docs/indicators/oscillators/ac/ac.md b/docs/indicators/oscillators/ac/ac.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/oscillators/ao/ao.md b/docs/indicators/oscillators/ao/ao.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/oscillators/aroon/aroon.md b/docs/indicators/oscillators/aroon/aroon.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/oscillators/bop/bop.md b/docs/indicators/oscillators/bop/bop.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/oscillators/cci/cci.md b/docs/indicators/oscillators/cci/cci.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/oscillators/cfo/cfo.md b/docs/indicators/oscillators/cfo/cfo.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/oscillators/cmo/cmo.md b/docs/indicators/oscillators/cmo/cmo.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/oscillators/rsi/rsi.md b/docs/indicators/oscillators/rsi/rsi.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/oscillators/rsx/rsx.md b/docs/indicators/oscillators/rsx/rsx.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/statistics/curvature/curvature.md b/docs/indicators/statistics/curvature/curvature.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/statistics/entropy/entropy.md b/docs/indicators/statistics/entropy/entropy.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/statistics/kurtosis/kurtosis.md b/docs/indicators/statistics/kurtosis/kurtosis.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/statistics/max/max.md b/docs/indicators/statistics/max/max.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/statistics/median/median.md b/docs/indicators/statistics/median/median.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/statistics/min/min.md b/docs/indicators/statistics/min/min.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/statistics/mode/mode.md b/docs/indicators/statistics/mode/mode.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/statistics/percentile/percentile.md b/docs/indicators/statistics/percentile/percentile.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/statistics/skew/skew.md b/docs/indicators/statistics/skew/skew.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/statistics/slope/slope.md b/docs/indicators/statistics/slope/slope.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/statistics/stddev/stddev.md b/docs/indicators/statistics/stddev/stddev.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/statistics/variance/variance.md b/docs/indicators/statistics/variance/variance.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/statistics/zscore/zscore.md b/docs/indicators/statistics/zscore/zscore.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/volatility/atr/atr.md b/docs/indicators/volatility/atr/atr.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/volatility/hv/hv.md b/docs/indicators/volatility/hv/hv.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/volatility/jvolty/jvolty.md b/docs/indicators/volatility/jvolty/jvolty.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/volatility/rv/rv.md b/docs/indicators/volatility/rv/rv.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/volatility/tr/tr.md b/docs/indicators/volatility/tr/tr.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/volatility/ui/ui.md b/docs/indicators/volatility/ui/ui.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/volatility/vc/vc.md b/docs/indicators/volatility/vc/vc.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/volatility/vov/vov.md b/docs/indicators/volatility/vov/vov.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/volatility/vr/vr.md b/docs/indicators/volatility/vr/vr.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/volatility/vs/vs.md b/docs/indicators/volatility/vs/vs.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/volume/adl/adl.md b/docs/indicators/volume/adl/adl.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/volume/adosc/adosc.md b/docs/indicators/volume/adosc/adosc.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/volume/aobv/aobv.md b/docs/indicators/volume/aobv/aobv.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/volume/cmf/cmf.md b/docs/indicators/volume/cmf/cmf.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/volume/eom/eom.md b/docs/indicators/volume/eom/eom.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/volume/kvo/kvo.md b/docs/indicators/volume/kvo/kvo.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/volume/mfi/mfi.md b/docs/indicators/volume/mfi/mfi.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/volume/nvi/nvi.md b/docs/indicators/volume/nvi/nvi.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/volume/obv/obv.md b/docs/indicators/volume/obv/obv.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/volume/pvi/pvi.md b/docs/indicators/volume/pvi/pvi.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/volume/pvo/pvo.md b/docs/indicators/volume/pvo/pvo.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/volume/pvol/pvol.md b/docs/indicators/volume/pvol/pvol.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/volume/pvr/pvr.md b/docs/indicators/volume/pvr/pvr.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/volume/pvt/pvt.md b/docs/indicators/volume/pvt/pvt.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/indicators/volume/tvi/tvi.md b/docs/indicators/volume/tvi/tvi.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/styles.css b/docs/styles.css deleted file mode 100644 index 5f30ab96..00000000 --- a/docs/styles.css +++ /dev/null @@ -1,1617 +0,0 @@ -:root { - /* Base Styles */ - box-sizing: border-box; - background-color: var(--base-background-color); - font-size: var(--base-font-size); - font-weight: var(--base-font-weight); - line-height: var(--base-line-height); - letter-spacing: var(--base-letter-spacing); - color: var(--base-color); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - /* Base Colors */ - --mono-hue: 201; - --mono-saturation: 18%; - --mono-shade3: hsl(var(--mono-hue), var(--mono-saturation), 13%); - --mono-shade2: hsl(var(--mono-hue), var(--mono-saturation), 15%); - --mono-shade1: hsl(var(--mono-hue), var(--mono-saturation), 17%); - --mono-base: hsl(var(--mono-hue), var(--mono-saturation), 19%); - --mono-tint1: hsl(var(--mono-hue), var(--mono-saturation), 25%); - --mono-tint2: hsl(var(--mono-hue), var(--mono-saturation), 35%); - --mono-tint3: hsl(var(--mono-hue), var(--mono-saturation), 43%); - - /* Theme Colors */ - --theme-hue: 204; - --theme-saturation: 90%; - --theme-lightness: 45%; - --theme-color: hsl(var(--theme-hue), var(--theme-saturation), var(--theme-lightness)); - - /* Base Variables */ - --base-background-color: var(--mono-base); - --base-color: #d3d3d3; - --base-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; - --base-font-size: 14px; - --base-font-weight: normal; - --base-line-height: 1.7; - - /* Modular Scale */ - --modular-scale: 1.0; - --modular-scale--2: calc(var(--modular-scale--1) / var(--modular-scale)); - --modular-scale--1: calc(var(--modular-scale-1) / var(--modular-scale)); - --modular-scale-1: 1rem; - --modular-scale-2: calc(var(--modular-scale-1) * var(--modular-scale)); - --modular-scale-3: calc(var(--modular-scale-2) * var(--modular-scale)); - --modular-scale-4: calc(var(--modular-scale-3) * var(--modular-scale)); - --modular-scale-5: calc(var(--modular-scale-4) * var(--modular-scale)); - - /* Font Sizes */ - --font-size-xxxl: var(--modular-scale-5); - --font-size-xxl: var(--modular-scale-4); - --font-size-xl: var(--modular-scale-3); - --font-size-l: var(--modular-scale-2); - --font-size-m: var(--modular-scale-1); - --font-size-s: var(--modular-scale--1); - --font-size-xs: var(--modular-scale--2); - - /* Headings */ - --heading-color: #fff; - --heading-font-weight: var(--strong-font-weight); - --heading-margin: 2.5rem 0 0; - --heading-h1-font-size: var(--font-size-xxl); - --heading-h2-font-size: var(--font-size-xl); - --heading-h3-font-size: var(--font-size-l); - --heading-h4-font-size: var(--font-size-m); - --heading-h5-font-size: var(--font-size-s); - --heading-h6-font-size: var(--font-size-xs); - - /* Blockquote */ - --blockquote-background: var(--mono-shade2); - --blockquote-border-color: var(--theme-color); - --blockquote-border-style: solid; - --blockquote-border-width: 0 0 0 4px; - --blockquote-border-radius: 0 var(--border-radius-m) var(--border-radius-m) 0; - --blockquote-padding: 1.5em; - - /* Code */ - --code-font-family: Inconsolata, Consolas, Menlo, Monaco, "Andale Mono WT", "Andale Mono", "Lucida Console", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace; - --code-font-size: calc(var(--font-size-m) * 0.95); - --code-font-weight: normal; - --code-tab-size: 4; - --code-block-border-radius: var(--border-radius-m); - --code-block-padding: 1.75em 1.5em 1.5em 1.5em; - --code-inline-background: var(--mono-tint1); - --code-inline-border-radius: var(--border-radius-s); - --code-inline-padding: 0.125em 0.4em; - - /* Notice */ - --notice-background: var(--mono-shade2); - --notice-border-radius: 0 var(--border-radius-m) var(--border-radius-m) 0; - --notice-border-width: 0 0 0 4px; - --notice-padding: 1em 1.5em 1em 3em; - - /* Table */ - --table-cell-border-color: var(--mono-tint1); - --table-cell-border-width: 1px 0; - --table-cell-padding: 0.75em 0.5em; - --table-row-odd-background: var(--mono-shade2); - - /* Layout */ - --content-max-width: 75em; - - /* Cover */ - --cover-margin: 0 auto; - --cover-max-width: 40em; - --cover-background-color: var(--base-background-color); - --cover-background-image: radial-gradient(ellipse at center bottom, var(--mono-tint3), transparent); - - /* Navbar */ - --navbar-menu-background: var(--mono-tint1); - --navbar-menu-border-radius: var(--border-radius-m); - --navbar-menu-padding: 0.5em; - - /* Sidebar */ - --sidebar-background: var(--mono-shade1); - --sidebar-border-color: var(--mono-tint1); - --sidebar-border-width: 0 1px 0 0; - --sidebar-padding: 0 25px; - --sidebar-width: 17rem; - - /* Search */ - --search-margin: 1.5rem 0 0; - --search-input-background-color: var(--mono-shade2); - --search-input-border-color: var(--mono-tint1); - --search-input-padding: 0.5em; - - /* Misc UI */ - --border-radius-s: 2px; - --border-radius-m: 4px; - --duration-fast: 0.25s; - --duration-medium: 0.5s; - --duration-slow: 1s; -} - -.github-corner{ - position:absolute; - z-index:40; - top:0; - right:0; - border-bottom:0; - text-decoration:none -} -.github-corner svg{ - height:70px; - width:70px; - fill:var(--theme-color); - color:var(--base-background-color) -} -.github-corner:hover .octo-arm{ - -webkit-animation:octocat-wave 560ms ease-in-out; - animation:octocat-wave 560ms ease-in-out -} -@-webkit-keyframes octocat-wave{ - 0%,100%{ - transform:rotate(0) - } - 20%,60%{ - transform:rotate(-25deg) - } - 40%,80%{ - transform:rotate(10deg) - } -} -@keyframes octocat-wave{ - 0%,100%{ - transform:rotate(0) - } - 20%,60%{ - transform:rotate(-25deg) - } - 40%,80%{ - transform:rotate(10deg) - } -} -.progress{ - position:fixed; - z-index:2147483647; - top:0; - left:0; - right:0; - height:3px; - width:0; - background-color:var(--theme-color); - transition:width var(--duration-fast),opacity calc(var(--duration-fast)*2) -} -body.ready-transition:after,body.ready-transition>*:not(.progress){ - opacity:0; - transition:opacity var(--spinner-transition-duration) -} -body.ready-transition:after{ - content:""; - position:absolute; - z-index:1000; - top:calc(50% - var(--spinner-size)/2); - left:calc(50% - var(--spinner-size)/2); - height:var(--spinner-size); - width:var(--spinner-size); - border:var(--spinner-track-width, 0) solid var(--spinner-track-color); - border-left-color:var(--theme-color); - border-radius:50%; - -webkit-animation:spinner var(--duration-slow) infinite linear; - animation:spinner var(--duration-slow) infinite linear -} -body.ready-transition.ready-spinner:after{ - opacity:1 -} -body.ready-transition.ready-fix:after{ - opacity:0 -} -body.ready-transition.ready-fix>*:not(.progress){ - opacity:1; - transition-delay:var(--spinner-transition-duration) -} -@-webkit-keyframes spinner{ - 0%{ - transform:rotate(0deg) - } - 100%{ - transform:rotate(360deg) - } -} -@keyframes spinner{ - 0%{ - transform:rotate(0deg) - } - 100%{ - transform:rotate(360deg) - } -} -*,*:before,*:after{ - box-sizing:inherit; - font-size:inherit; - -webkit-overflow-scrolling:touch; - -webkit-tap-highlight-color:rgba(0,0,0,0); - -webkit-text-size-adjust:none; - -webkit-touch-callout:none -} -html,button,input,optgroup,select,textarea{ - font-family:var(--base-font-family) -} -button,input,optgroup,select,textarea{ - font-size:100%; - margin:0 -} -a{ - text-decoration:none; - -webkit-text-decoration-skip:ink; - text-decoration-skip-ink:auto -} -body{ - margin:0 -} -hr{ - height:0; - margin:2em 0; - border:none; - border-bottom:var(--hr-border, 0) -} -img{ - max-width:100%; - border:0 -} -main{ - display:block; - position:relative; - overflow-x:hidden; - min-height:100vh -} -main.hidden{ - display:none -} -mark{ - background:var(--mark-background); - color:var(--mark-color) -} -pre{ - font-family:var(--pre-font-family); - font-size:var(--pre-font-size); - font-weight:var(--pre-font-weight); - line-height:var(--pre-line-height) -} -small{ - display:inline-block; - font-size:var(--small-font-size) -} -strong{ - font-weight:var(--strong-font-weight); - color:var(--strong-color, currentColor) -} -sub,sup{ - font-size:var(--subsup-font-size); - line-height:0; - position:relative; - vertical-align:baseline -} -sub{ - bottom:-0.25em -} -sup{ - top:-0.5em -} -body:not([data-platform^=Mac]) *{ - scrollbar-color:hsla(var(--mono-hue), var(--mono-saturation), 50%, 0.3) hsla(var(--mono-hue), var(--mono-saturation), 50%, 0.1); - scrollbar-width:thin -} -body:not([data-platform^=Mac]) * ::-webkit-scrollbar{ - width:5px; - height:5px -} -body:not([data-platform^=Mac]) * ::-webkit-scrollbar-thumb{ - background:hsla(var(--mono-hue), var(--mono-saturation), 50%, 0.3) -} -body:not([data-platform^=Mac]) * ::-webkit-scrollbar-track{ - background:hsla(var(--mono-hue), var(--mono-saturation), 50%, 0.1) -} -::-moz-selection{ - background: var(--selection-color, #0074d9); - color: #ffffff; -} -::selection{ - background: var(--selection-color, #0074d9); - color: #ffffff; -} -.emoji{ - height:var(--emoji-size); - vertical-align:middle -} -.task-list-item{ - list-style:none -} -.task-list-item input{ - margin-right:.5em; - margin-left:0; - vertical-align:.075em -} -.markdown-section code[class*=lang-],.markdown-section pre[data-lang]{ - font-family:var(--code-font-family); - font-size:var(--code-font-size); - font-weight:var(--code-font-weight); - letter-spacing:normal; - line-height:var(--code-block-line-height); - -moz-tab-size:var(--code-tab-size); - -o-tab-size:var(--code-tab-size); - tab-size:var(--code-tab-size); - text-align:left; - white-space:pre; - word-spacing:normal; - word-wrap:normal; - word-break:normal; - -webkit-hyphens:none; - hyphens:none -} -.markdown-section pre[data-lang]{ - position:relative; - overflow:hidden; - margin:var(--code-block-margin); - padding:0; - border-radius:var(--code-block-border-radius) -} -.markdown-section pre[data-lang]::after{ - content:attr(data-lang); - position:absolute; - top:.75em; - right:.75em; - opacity:.6; - color:inherit; - font-size:var(--font-size-s); - line-height:1 -} -.markdown-section pre[data-lang] code{ - display:block; - overflow:auto; - padding:var(--code-block-padding) -} -code[class*=lang-],pre[data-lang]{ - color:var(--code-theme-text) -} -pre[data-lang]::-moz-selection,pre[data-lang] ::-moz-selection,code[class*=lang-]::-moz-selection,code[class*=lang-] ::-moz-selection{ - background:var(--code-theme-selection, var(--selection-color)) -} -pre[data-lang]::selection,pre[data-lang] ::selection,code[class*=lang-]::selection,code[class*=lang-] ::selection{ - background:var(--code-theme-selection, var(--selection-color)) -} -:not(pre)>code[class*=lang-],pre[data-lang]{ - background:var(--code-theme-background) -} -.namespace{ - opacity:.7 -} -.token.comment,.token.prolog,.token.doctype,.token.cdata{ - color:var(--code-theme-comment) -} -.token.punctuation{ - color:var(--code-theme-punctuation) -} -.token.property,.token.tag,.token.boolean,.token.number,.token.constant,.token.symbol,.token.deleted{ - color:var(--code-theme-tag) -} -.token.selector,.token.attr-name,.token.string,.token.char,.token.builtin,.token.inserted{ - color:var(--code-theme-selector) -} -.token.operator,.token.entity,.token.url,.language-css .token.string,.style .token.string{ - color:var(--code-theme-operator) -} -.token.atrule,.token.attr-value,.token.keyword{ - color:var(--code-theme-keyword) -} -.token.function{ - color:var(--code-theme-function) -} -.token.regex,.token.important,.token.variable{ - color:var(--code-theme-variable) -} -.token.important,.token.bold{ - font-weight:bold -} -.token.italic{ - font-style:italic -} -.token.entity{ - cursor:help -} -.markdown-section{ - position:relative; - max-width:var(--content-max-width); - margin:0 auto; - padding:2rem 45px -} -.app-nav:not(:empty)~main .markdown-section{ - padding-top:3.5rem -} -.markdown-section figure,.markdown-section p,.markdown-section ol,.markdown-section ul{ - margin:1em 0 -} -.markdown-section ol,.markdown-section ul{ - padding-left:1.5rem -} -.markdown-section ol ol,.markdown-section ol ul,.markdown-section ul ol,.markdown-section ul ul{ - margin-top:.15rem; - margin-bottom:.15rem -} -.markdown-section a{ - border-bottom:var(--link-border-bottom); - color:var(--link-color); - -webkit-text-decoration:var(--link-text-decoration); - text-decoration:var(--link-text-decoration); - -webkit-text-decoration-color:var(--link-text-decoration-color); - text-decoration-color:var(--link-text-decoration-color) -} -.markdown-section a:hover{ - border-bottom:var(--link-border-bottom--hover, var(--link-border-bottom, 0)); - color:var(--link-color--hover, var(--link-color)); - -webkit-text-decoration:var(--link-text-decoration--hover, var(--link-text-decoration)); - text-decoration:var(--link-text-decoration--hover, var(--link-text-decoration)); - -webkit-text-decoration-color:var(--link-text-decoration-color--hover, var(--link-text-decoration-color)); - text-decoration-color:var(--link-text-decoration-color--hover, var(--link-text-decoration-color)) -} -.markdown-section a.anchor{ - border-bottom:0; - color:inherit; - text-decoration:none -} -.markdown-section a.anchor:hover{ - text-decoration:underline -} -.markdown-section blockquote{ - overflow:visible; - margin:2em 0; - padding:var(--blockquote-padding); - border-width:var(--blockquote-border-width, 0); - border-style:var(--blockquote-border-style); - border-color:var(--blockquote-border-color); - border-radius:var(--blockquote-border-radius); - background:var(--blockquote-background); - color:var(--blockquote-color); - font-family:var(--blockquote-font-family); - font-size:var(--blockquote-font-size); - font-style:var(--blockquote-font-style); - font-weight:var(--blockquote-font-weight); - quotes:"“" "”" "‘" "’" -} -.markdown-section blockquote em{ - font-family:var(--blockquote-em-font-family); - font-size:var(--blockquote-em-font-size); - font-style:var(--blockquote-em-font-style); - font-weight:var(--blockquote-em-font-weight) -} -.markdown-section blockquote p:first-child{ - margin-top:0 -} -.markdown-section blockquote p:first-child:before,.markdown-section blockquote p:first-child:after{ - color:var(--blockquote-quotes-color); - font-family:var(--blockquote-quotes-font-family); - font-size:var(--blockquote-quotes-font-size); - line-height:0 -} -.markdown-section blockquote p:first-child:before{ - content:var(--blockquote-quotes-open); - margin-right:.15em; - vertical-align:-0.45em -} -.markdown-section blockquote p:first-child:after{ - content:var(--blockquote-quotes-close); - margin-left:.15em; - vertical-align:-0.55em -} -.markdown-section blockquote p:last-child{ - margin-bottom:0 -} -.markdown-section code{ - font-family:var(--code-font-family); - font-size:var(--code-font-size); - font-weight:var(--code-font-weight); - line-height:inherit -} -.markdown-section code:not([class*=lang-]):not([class*=language-]){ - margin:var(--code-inline-margin); - padding:var(--code-inline-padding); - border-radius:var(--code-inline-border-radius); - background:var(--code-inline-background); - color:var(--code-inline-color, currentColor); - white-space:nowrap -} -.markdown-section h1:first-child,.markdown-section h2:first-child,.markdown-section h3:first-child,.markdown-section h4:first-child,.markdown-section h5:first-child,.markdown-section h6:first-child{ - margin-top:0 -} -.markdown-section h1 a[data-id],.markdown-section h2 a[data-id],.markdown-section h3 a[data-id],.markdown-section h4 a[data-id],.markdown-section h5 a[data-id],.markdown-section h6 a[data-id]{ - display:inline-block -} -.markdown-section h1 code,.markdown-section h2 code,.markdown-section h3 code,.markdown-section h4 code,.markdown-section h5 code,.markdown-section h6 code{ - font-size:.875em -} -.markdown-section h1+h2,.markdown-section h1+h3,.markdown-section h1+h4,.markdown-section h1+h5,.markdown-section h1+h6,.markdown-section h2+h3,.markdown-section h2+h4,.markdown-section h2+h5,.markdown-section h2+h6,.markdown-section h3+h4,.markdown-section h3+h5,.markdown-section h3+h6,.markdown-section h4+h5,.markdown-section h4+h6,.markdown-section h5+h6{ - margin-top:1rem -} -.markdown-section h1{ - margin:var(--heading-h1-margin, var(--heading-margin)); - padding:var(--heading-h1-padding, var(--heading-padding)); - border-width:var(--heading-h1-border-width, 0); - border-style:var(--heading-h1-border-style); - border-color:var(--heading-h1-border-color); - font-family:var(--heading-h1-font-family, var(--heading-font-family)); - font-size:var(--heading-h1-font-size); - font-weight:var(--heading-h1-font-weight, var(--heading-font-weight)); - line-height:var(--base-line-height); - color:var(--heading-h1-color, var(--heading-color)) -} -.markdown-section h2{ - margin:var(--heading-h2-margin, var(--heading-margin)); - padding:var(--heading-h2-padding, var(--heading-padding)); - border-width:var(--heading-h2-border-width, 0); - border-style:var(--heading-h2-border-style); - border-color:var(--heading-h2-border-color); - font-family:var(--heading-h2-font-family, var(--heading-font-family)); - font-size:var(--heading-h2-font-size); - font-weight:var(--heading-h2-font-weight, var(--heading-font-weight)); - line-height:var(--base-line-height); - color:var(--heading-h2-color, var(--heading-color)) -} -.markdown-section h3{ - margin:var(--heading-h3-margin, var(--heading-margin)); - padding:var(--heading-h3-padding, var(--heading-padding)); - border-width:var(--heading-h3-border-width, 0); - border-style:var(--heading-h3-border-style); - border-color:var(--heading-h3-border-color); - font-family:var(--heading-h3-font-family, var(--heading-font-family)); - font-size:var(--heading-h3-font-size); - font-weight:var(--heading-h3-font-weight, var(--heading-font-weight)); - color:var(--heading-h3-color, var(--heading-color)) -} -.markdown-section h4{ - margin:var(--heading-h4-margin, var(--heading-margin)); - padding:var(--heading-h4-padding, var(--heading-padding)); - border-width:var(--heading-h4-border-width, 0); - border-style:var(--heading-h4-border-style); - border-color:var(--heading-h4-border-color); - font-family:var(--heading-h4-font-family, var(--heading-font-family)); - font-size:var(--heading-h4-font-size); - font-weight:var(--heading-h4-font-weight, var(--heading-font-weight)); - color:var(--heading-h4-color, var(--heading-color)) -} -.markdown-section h5{ - margin:var(--heading-h5-margin, var(--heading-margin)); - padding:var(--heading-h5-padding, var(--heading-padding)); - border-width:var(--heading-h5-border-width, 0); - border-style:var(--heading-h5-border-style); - border-color:var(--heading-h5-border-color); - font-family:var(--heading-h5-font-family, var(--heading-font-family)); - font-size:var(--heading-h5-font-size); - font-weight:var(--heading-h5-font-weight, var(--heading-font-weight)); - color:var(--heading-h5-color, var(--heading-color)) -} -.markdown-section h6{ - margin:var(--heading-h6-margin, var(--heading-margin)); - padding:var(--heading-h6-padding, var(--heading-padding)); - border-width:var(--heading-h6-border-width, 0); - border-style:var(--heading-h6-border-style); - border-color:var(--heading-h6-border-color); - font-family:var(--heading-h6-font-family, var(--heading-font-family)); - font-size:var(--heading-h6-font-size); - font-weight:var(--heading-h6-font-weight, var(--heading-font-weight)); - color:var(--heading-h6-color, var(--heading-color)) -} -.markdown-section iframe{ - margin:1em 0 -} -.markdown-section img{ - max-width:100% -} -.markdown-section kbd{ - display:inline-block; - min-width:var(--kbd-min-width); - margin:var(--kbd-margin); - padding:var(--kbd-padding); - border:var(--kbd-border); - border-radius:var(--kbd-border-radius); - background:var(--kbd-background); - font-family:inherit; - font-size:var(--kbd-font-size); - text-align:center; - letter-spacing:0; - line-height:1; - color:var(--kbd-color) -} -.markdown-section kbd+kbd{ - margin-left:-0.15em -} -.markdown-section table{ - display:block; - overflow:auto; - margin:1rem 0; - border-spacing:0; - border-collapse:collapse -} -.markdown-section th:not([align]){ - text-align:left -} -.markdown-section thead{ - border-color:var(--table-head-border-color); - border-style:solid; - border-width:var(--table-head-border-width, 0); - background:var(--table-head-background) -} -.markdown-section th{ - font-weight:var(--table-head-font-weight, 700); - color:var(--strong-color); -} -.markdown-section td{ - border-color:var(--table-cell-border-color); - border-style:solid; - border-width:var(--table-cell-border-width, 0) -} -.markdown-section td, -.markdown-section th { - padding:var(--table-cell-padding); - line-height: 1.1; /* Reduced from default 1.7 */ -} -.markdown-section th strong, -.markdown-section th b, -.markdown-section th em, -.markdown-section td strong, -.markdown-section td b, -.markdown-section td em { - font-size: 1.2em; - font-weight: var(--table-head-font-weight, 700); - color: var(--strong-color); - line-height: 2.0; -} -.markdown-section tbody{ - border-color:var(--table-body-border-color); - border-style:solid; - border-width:var(--table-body-border-width, 0) -} -.markdown-section tbody tr:nth-child(odd){ - background:var(--table-row-odd-background) -} -.markdown-section tbody tr:nth-child(even){ - background:var(--table-row-even-background) -} -.markdown-section>ul .task-list-item{ - margin-left:-1.25em -} -.markdown-section>ul .task-list-item .task-list-item{ - margin-left:0 -} -.markdown-section .table-wrapper{ - overflow-x:auto -} -.markdown-section .table-wrapper table{ - display:table; - width:100% -} -.markdown-section .table-wrapper td::before{ - display:none -} -@media(max-width: 30em){ - .markdown-section .table-wrapper tbody,.markdown-section .table-wrapper tr,.markdown-section .table-wrapper td{ - display:block - } - .markdown-section .table-wrapper th,.markdown-section .table-wrapper td{ - border:none - } - .markdown-section .table-wrapper thead{ - display:none - } - .markdown-section .table-wrapper tr{ - border-color:var(--table-cell-border-color); - border-style:solid; - border-width:var(--table-cell-border-width, 0); - padding:var(--table-cell-padding) - } - .markdown-section .table-wrapper tr:not(:last-child){ - border-bottom:0 - } - .markdown-section .table-wrapper td{ - padding:.15em 0 .15em 8em - } - .markdown-section .table-wrapper td::before{ - display:inline-block; - width:8em; - margin-left:-8em; - font-weight:bold; - text-align:left - } -} -.markdown-section .tip,.markdown-section .warn{ - position:relative; - margin:2em 0; - padding:var(--notice-padding); - border-width:var(--notice-border-width, 0); - border-style:var(--notice-border-style); - border-color:var(--notice-border-color); - border-radius:var(--notice-border-radius); - background:var(--notice-background); - font-family:var(--notice-font-family); - font-weight:var(--notice-font-weight); - color:var(--notice-color) -} -.markdown-section .tip:before,.markdown-section .warn:before{ - display:inline-block; - position:var(--notice-before-position, relative); - top:var(--notice-before-top); - left:var(--notice-before-left); - height:var(--notice-before-height); - width:var(--notice-before-width); - margin:var(--notice-before-margin); - padding:var(--notice-before-padding); - border-radius:var(--notice-before-border-radius); - line-height:var(--notice-before-line-height); - font-family:var(--notice-before-font-family); - font-size:var(--notice-before-font-size); - font-weight:var(--notice-before-font-weight); - text-align:center -} -.markdown-section .tip{ - border-width:var(--notice-important-border-width, var(--notice-border-width, 0)); - border-style:var(--notice-important-border-style, var(--notice-border-style)); - border-color:var(--notice-important-border-color, var(--notice-border-color)); - background:var(--notice-important-background, var(--notice-background)); - color:var(--notice-important-color, var(--notice-color)) -} -.markdown-section .tip:before{ - content:var(--notice-important-before-content, var(--notice-before-content)); - background:var(--notice-important-before-background, var(--notice-before-background)); - color:var(--notice-important-before-color, var(--notice-before-color)) -} -.markdown-section .warn{ - border-width:var(--notice-tip-border-width, var(--notice-border-width, 0)); - border-style:var(--notice-tip-border-style, var(--notice-border-style)); - border-color:var(--notice-tip-border-color, var(--notice-border-color)); - background:var(--notice-tip-background, var(--notice-background)); - color:var(--notice-tip-color, var(--notice-color)) -} -.markdown-section .warn:before{ - content:var(--notice-tip-before-content, var(--notice-before-content)); - background:var(--notice-tip-before-background, var(--notice-before-background)); - color:var(--notice-tip-before-color, var(--notice-before-color)) -} -.cover{ - display:none; - position:relative; - z-index:20; - min-height:100vh; - flex-direction:column; - align-items:center; - justify-content:center; - padding:calc(var(--cover-border-inset, 0px) + var(--cover-border-width, 0px)); - color:var(--cover-color); - text-align:var(--cover-text-align) -} -@media screen and (-ms-high-contrast: active),screen and (-ms-high-contrast: none){ - .cover{ - height:100vh - } -} -.cover:before,.cover:after{ - content:""; - position:absolute -} -.cover:before{ - top:0; - bottom:0; - left:0; - right:0; - background-blend-mode:var(--cover-background-blend-mode); - background-color:var(--cover-background-color); - background-image:var(--cover-background-image); - background-position:var(--cover-background-position); - background-repeat:var(--cover-background-repeat); - background-size:var(--cover-background-size) -} -.cover:after{ - top:var(--cover-border-inset, 0); - bottom:var(--cover-border-inset, 0); - left:var(--cover-border-inset, 0); - right:var(--cover-border-inset, 0); - border-width:var(--cover-border-width, 0); - border-style:solid; - border-color:var(--cover-border-color) -} -.cover a{ - border-bottom:var(--cover-link-border-bottom); - color:var(--cover-link-color); - -webkit-text-decoration:var(--cover-link-text-decoration); - text-decoration:var(--cover-link-text-decoration); - -webkit-text-decoration-color:var(--cover-link-text-decoration-color); - text-decoration-color:var(--cover-link-text-decoration-color) -} -.cover a:hover{ - border-bottom:var(--cover-link-border-bottom--hover, var(--cover-link-border-bottom)); - color:var(--cover-link-color--hover, var(--cover-link-color)); - -webkit-text-decoration:var(--cover-link-text-decoration--hover, var(--cover-link-text-decoration)); - text-decoration:var(--cover-link-text-decoration--hover, var(--cover-link-text-decoration)); - -webkit-text-decoration-color:var(--cover-link-text-decoration-color--hover, var(--cover-link-text-decoration-color)); - text-decoration-color:var(--cover-link-text-decoration-color--hover, var(--cover-link-text-decoration-color)) -} -.cover h1{ - color:var(--cover-heading-color); - position:relative; - margin:0; - font-size:var(--cover-heading-font-size); - font-weight:var(--cover-heading-font-weight); - line-height:1.2 -} -.cover h1 a,.cover h1 a:hover{ - display:block; - border-bottom:none; - color:inherit; - text-decoration:none -} -.cover h1 small{ - position:absolute; - bottom:0; - margin-left:.5em -} -.cover h1 span{ - font-size:calc(var(--cover-heading-font-size-min)*1px) -} -@media(min-width: 26em){ - .cover h1 span{ - font-size:calc(var(--cover-heading-font-size-min)*1px + (var(--cover-heading-font-size-max) - var(--cover-heading-font-size-min))*(100vw - 420px)/604) - } -} -@media(min-width: 64em){ - .cover h1 span{ - font-size:calc(var(--cover-heading-font-size-max)*1px) - } -} -.cover blockquote{ - margin:0; - color:var(--cover-blockquote-color); - font-size:var(--cover-blockquote-font-size) -} -.cover blockquote a{ - color:inherit -} -.cover ul{ - padding:0; - list-style-type:none -} -.cover .cover-main{ - position:relative; - z-index:1; - max-width:var(--cover-max-width); - margin:var(--cover-margin); - padding:0 45px -} -.cover .cover-main>p:last-child{ - margin:1.25em -0.25em -} -.cover .cover-main>p:last-child a{ - display:block; - margin:.375em .25em; - padding:var(--cover-button-padding); - border:var(--cover-button-border); - border-radius:var(--cover-button-border-radius); - box-shadow:var(--cover-button-box-shadow); - background:var(--cover-button-background); - text-align:center; - -webkit-text-decoration:var(--cover-button-text-decoration); - text-decoration:var(--cover-button-text-decoration); - -webkit-text-decoration-color:var(--cover-button-text-decoration-color); - text-decoration-color:var(--cover-button-text-decoration-color); - color:var(--cover-button-color); - white-space:nowrap; - transition:var(--cover-button-transition) -} -.cover .cover-main>p:last-child a:hover{ - border:var(--cover-button-border--hover, var(--cover-button-border)); - box-shadow:var(--cover-button-box-shadow--hover, var(--cover-button-box-shadow)); - background:var(--cover-button-background--hover, var(--cover-button-background)); - -webkit-text-decoration:var(--cover-button-text-decoration--hover, var(--cover-button-text-decoration)); - text-decoration:var(--cover-button-text-decoration--hover, var(--cover-button-text-decoration)); - -webkit-text-decoration-color:var(--cover-button-text-decoration-color--hover, var(--cover-button-text-decoration-color)); - text-decoration-color:var(--cover-button-text-decoration-color--hover, var(--cover-button-text-decoration-color)); - color:var(--cover-button-color--hover, var(--cover-button-color)) -} -.cover .cover-main>p:last-child a:first-child{ - border:var(--cover-button-primary-border, var(--cover-button-border)); - box-shadow:var(--cover-button-primary-box-shadow, var(--cover-button-box-shadow)); - background:var(--cover-button-primary-background, var(--cover-button-background)); - -webkit-text-decoration:var(--cover-button-primary-text-decoration, var(--cover-button-text-decoration)); - text-decoration:var(--cover-button-primary-text-decoration, var(--cover-button-text-decoration)); - -webkit-text-decoration-color:var(--cover-button-primary-text-decoration-color, var(--cover-button-text-decoration-color)); - text-decoration-color:var(--cover-button-primary-text-decoration-color, var(--cover-button-text-decoration-color)); - color:var(--cover-button-primary-color, var(--cover-button-color)) -} -.cover .cover-main>p:last-child a:first-child:hover{ - border:var(--cover-button-primary-border--hover, var(--cover-button-border--hover, var(--cover-button-primary-border, var(--cover-button-border)))); - box-shadow:var(--cover-button-primary-box-shadow--hover, var(--cover-button-box-shadow--hover, var(--cover-button-primary-box-shadow, var(--cover-button-box-shadow)))); - background:var(--cover-button-primary-background--hover, var(--cover-button-background--hover, var(--cover-button-primary-background, var(--cover-button-background)))); - -webkit-text-decoration:var(--cover-button-primary-text-decoration--hover, var(--cover-button-text-decoration--hover, var(--cover-button-primary-text-decoration, var(--cover-button-text-decoration)))); - text-decoration:var(--cover-button-primary-text-decoration--hover, var(--cover-button-text-decoration--hover, var(--cover-button-primary-text-decoration, var(--cover-button-text-decoration)))); - -webkit-text-decoration-color:var(--cover-button-primary-text-decoration-color--hover, var(--cover-button-text-decoration-color--hover, var(--cover-button-primary-text-decoration-color, var(--cover-button-text-decoration-color)))); - text-decoration-color:var(--cover-button-primary-text-decoration-color--hover, var(--cover-button-text-decoration-color--hover, var(--cover-button-primary-text-decoration-color, var(--cover-button-text-decoration-color)))); - color:var(--cover-button-primary-color--hover, var(--cover-button-color--hover, var(--cover-button-primary-color, var(--cover-button-color)))) -} -@media(min-width: 30.01em){ - .cover .cover-main>p:last-child a{ - display:inline-block - } -} -.cover .mask{ - visibility:var(--cover-background-mask-visibility, hidden); - position:absolute; - top:0; - bottom:0; - left:0; - right:0; - background-color:var(--cover-background-mask-color); - opacity:var(--cover-background-mask-opacity) -} -.cover.has-mask .mask{ - visibility:visible -} -.cover.show{ - display:flex -} -.app-nav{ - position:absolute; - z-index:30; - top:calc(35px - .5em*var(--base-line-height)); - left:45px; - right:80px; - text-align:right -} -.app-nav.no-badge{ - right:45px -} -.app-nav li>img,.app-nav li>a>img{ - margin-top:-0.25em; - vertical-align:middle -} -.app-nav li>img:first-child,.app-nav li>a>img:first-child{ - margin-right:.5em -} -.app-nav ul,.app-nav li{ - margin:0; - padding:0; - list-style:none -} -.app-nav li{ - position:relative -} -.app-nav li a{ - display:block; - line-height:1; - transition:var(--navbar-root-transition) -} -.app-nav>ul>li{ - display:inline-block; - margin:var(--navbar-root-margin) -} -.app-nav>ul>li:first-child{ - margin-left:0 -} -.app-nav>ul>li:last-child{ - margin-right:0 -} -.app-nav>ul>li>a,.app-nav>ul>li>span{ - padding:var(--navbar-root-padding); - border-width:var(--navbar-root-border-width, 0); - border-style:var(--navbar-root-border-style); - border-color:var(--navbar-root-border-color); - border-radius:var(--navbar-root-border-radius); - background:var(--navbar-root-background); - color:var(--navbar-root-color); - -webkit-text-decoration:var(--navbar-root-text-decoration); - text-decoration:var(--navbar-root-text-decoration); - -webkit-text-decoration-color:var(--navbar-root-text-decoration-color); - text-decoration-color:var(--navbar-root-text-decoration-color) -} -.app-nav>ul>li>a:hover,.app-nav>ul>li>span:hover{ - background:var(--navbar-root-background--hover, var(--navbar-root-background)); - border-style:var(--navbar-root-border-style--hover, var(--navbar-root-border-style)); - border-color:var(--navbar-root-border-color--hover, var(--navbar-root-border-color)); - color:var(--navbar-root-color--hover, var(--navbar-root-color)); - -webkit-text-decoration:var(--navbar-root-text-decoration--hover, var(--navbar-root-text-decoration)); - text-decoration:var(--navbar-root-text-decoration--hover, var(--navbar-root-text-decoration)); - -webkit-text-decoration-color:var(--navbar-root-text-decoration-color--hover, var(--navbar-root-text-decoration-color)); - text-decoration-color:var(--navbar-root-text-decoration-color--hover, var(--navbar-root-text-decoration-color)) -} -.app-nav>ul>li>a:not(:last-child),.app-nav>ul>li>span:not(:last-child){ - padding:var(--navbar-menu-root-padding, var(--navbar-root-padding)); - background:var(--navbar-menu-root-background, var(--navbar-root-background)) -} -.app-nav>ul>li>a:not(:last-child):hover,.app-nav>ul>li>span:not(:last-child):hover{ - background:var(--navbar-menu-root-background--hover, var(--navbar-menu-root-background, var(--navbar-root-background--hover, var(--navbar-root-background)))) -} -.app-nav>ul>li>a.active{ - background:var(--navbar-root-background--active, var(--navbar-root-background)); - border-style:var(--navbar-root-border-style--active, var(--navbar-root-border-style)); - border-color:var(--navbar-root-border-color--active, var(--navbar-root-border-color)); - color:var(--navbar-root-color--active, var(--navbar-root-color)); - -webkit-text-decoration:var(--navbar-root-text-decoration--active, var(--navbar-root-text-decoration)); - text-decoration:var(--navbar-root-text-decoration--active, var(--navbar-root-text-decoration)); - -webkit-text-decoration-color:var(--navbar-root-text-decoration-color--active, var(--navbar-root-text-decoration-color)); - text-decoration-color:var(--navbar-root-text-decoration-color--active, var(--navbar-root-text-decoration-color)) -} -.app-nav>ul>li>a.active:not(:last-child):hover{ - background:var(--navbar-menu-root-background--active, var(--navbar-menu-root-background, var(--navbar-root-background--active, var(--navbar-root-background)))) -} -.app-nav>ul>li ul{ - visibility:hidden; - position:absolute; - top:100%; - right:50%; - overflow-y:auto; - box-sizing:border-box; - max-height:50vh; - padding:var(--navbar-menu-padding); - border-width:var(--navbar-menu-border-width, 0); - border-style:solid; - border-color:var(--navbar-menu-border-color); - border-radius:var(--navbar-menu-border-radius); - background:var(--navbar-menu-background); - box-shadow:var(--navbar-menu-box-shadow); - text-align:left; - white-space:nowrap; - opacity:0; - transform:translate(50%, -0.35em); - transition:var(--navbar-menu-transition) -} -.app-nav>ul>li ul li{ - white-space:nowrap -} -.app-nav>ul>li ul a{ - margin:var(--navbar-menu-link-margin); - padding:var(--navbar-menu-link-padding); - border-width:var(--navbar-menu-link-border-width, 0); - border-style:var(--navbar-menu-link-border-style); - border-color:var(--navbar-menu-link-border-color); - border-radius:var(--navbar-menu-link-border-radius); - background:var(--navbar-menu-link-background); - color:var(--navbar-menu-link-color); - -webkit-text-decoration:var(--navbar-menu-link-text-decoration); - text-decoration:var(--navbar-menu-link-text-decoration); - -webkit-text-decoration-color:var(--navbar-menu-link-text-decoration-color); - text-decoration-color:var(--navbar-menu-link-text-decoration-color) -} -.app-nav>ul>li ul a:hover{ - background:var(--navbar-menu-link-background--hover, var(--navbar-menu-link-background)); - border-style:var(--navbar-menu-link-border-style--hover, var(--navbar-menu-link-border-style)); - border-color:var(--navbar-menu-link-border-color--hover, var(--navbar-menu-link-border-color)); - color:var(--navbar-menu-link-color--hover, var(--navbar-menu-link-color)); - -webkit-text-decoration:var(--navbar-menu-link-text-decoration--hover, var(--navbar-menu-link-text-decoration)); - text-decoration:var(--navbar-menu-link-text-decoration--hover, var(--navbar-menu-link-text-decoration)); - -webkit-text-decoration-color:var(--navbar-menu-link-text-decoration-color--hover, var(--navbar-menu-link-text-decoration-color)); - text-decoration-color:var(--navbar-menu-link-text-decoration-color--hover, var(--navbar-menu-link-text-decoration-color)) -} -.app-nav>ul>li ul a.active{ - background:var(--navbar-menu-link-background--active, var(--navbar-menu-link-background)); - border-style:var(--navbar-menu-link-border-style--active, var(--navbar-menu-link-border-style)); - border-color:var(--navbar-menu-link-border-color--active, var(--navbar-menu-link-border-color)); - color:var(--navbar-menu-link-color--active, var(--navbar-menu-link-color)); - -webkit-text-decoration:var(--navbar-menu-link-text-decoration--active, var(--navbar-menu-link-text-decoration)); - text-decoration:var(--navbar-menu-link-text-decoration--active, var(--navbar-menu-link-text-decoration)); - -webkit-text-decoration-color:var(--navbar-menu-link-text-decoration-color--active, var(--navbar-menu-link-text-decoration-color)); - text-decoration-color:var(--navbar-menu-link-text-decoration-color--active, var(--navbar-menu-link-text-decoration-color)) -} -.app-nav>ul>li:hover ul,.app-nav>ul>li:focus ul,.app-nav>ul>li.focus-within ul{ - visibility:visible; - opacity:1; - transform:translate(50%, 0) -} -@media(min-width: 48em){ - nav.app-nav{ - margin-left:var(--sidebar-width) - } -} -.sidebar,.sidebar-toggle,.sidebar+.content{ - transition:all var(--sidebar-transition-duration) ease-out -} -@media(min-width: 48em){ - .sidebar+.content{ - margin-left:var(--sidebar-width) - } -} -.sidebar{ - display:flex; - flex-direction:column; - position:fixed; - z-index:10; - top:0; - right:100%; - overflow-x:hidden; - overflow-y:auto; - height:100vh; - width:var(--sidebar-width); - padding:var(--sidebar-padding); - border-width:var(--sidebar-border-width); - border-style:solid; - border-color:var(--sidebar-border-color); - background:var(--sidebar-background) -} -.sidebar>h1{ - margin:0; - margin:var(--sidebar-name-margin); - padding:var(--sidebar-name-padding); - background:var(--sidebar-name-background); - color:var(--sidebar-name-color); - font-family:var(--sidebar-name-font-family); - font-size:var(--sidebar-name-font-size); - font-weight:var(--sidebar-name-font-weight); - text-align:var(--sidebar-name-text-align) -} -.sidebar>h1 img{ - max-width:100% -} -.sidebar>h1 .app-name-link{ - color:var(--sidebar-name-color) -} -body:not([data-platform^=Mac]) .sidebar::-webkit-scrollbar{ - width:5px -} -body:not([data-platform^=Mac]) .sidebar::-webkit-scrollbar-thumb{ - border-radius:50vw -} -@media(min-width: 48em){ - .sidebar{ - position:absolute; - transform:translateX(var(--sidebar-width)) - } -} -@media print{ - .sidebar{ - display:none - } -} -.sidebar-nav,.sidebar nav{ - order:1; - margin:var(--sidebar-nav-margin); - padding:var(--sidebar-nav-padding); - background:var(--sidebar-nav-background) -} -.sidebar-nav ul,.sidebar nav ul{ - margin:0; - padding:0; - list-style:none -} -.sidebar-nav ul ul,.sidebar nav ul ul{ - margin-left:var(--sidebar-nav-indent) -} -.sidebar-nav a,.sidebar nav a{ - display:block; - overflow:hidden; - margin:var(--sidebar-nav-link-margin); - padding:var(--sidebar-nav-link-padding); - border-width:var(--sidebar-nav-link-border-width, 0); - border-style:var(--sidebar-nav-link-border-style); - border-color:var(--sidebar-nav-link-border-color); - border-radius:var(--sidebar-nav-link-border-radius); - background:var(--sidebar-nav-link-background); - color:var(--sidebar-nav-link-color); - font-weight:var(--sidebar-nav-link-font-weight); - white-space:nowrap; - -webkit-text-decoration:var(--sidebar-nav-link-text-decoration); - text-decoration:var(--sidebar-nav-link-text-decoration); - -webkit-text-decoration-color:var(--sidebar-nav-link-text-decoration-color); - text-decoration-color:var(--sidebar-nav-link-text-decoration-color); - text-overflow:ellipsis; - transition:var(--sidebar-nav-link-transition) -} -.sidebar-nav a img,.sidebar nav a img{ - margin-top:-0.25em; - vertical-align:middle -} -.sidebar-nav a img:first-child,.sidebar nav a img:first-child{ - margin-right:.5em -} -.sidebar-nav a:hover,.sidebar nav a:hover{ - border-width:var(--sidebar-nav-link-border-width--hover, var(--sidebar-nav-link-border-width, 0)); - border-style:var(--sidebar-nav-link-border-style--hover, var(--sidebar-nav-link-border-style)); - border-color:var(--sidebar-nav-link-border-color--hover, var(--sidebar-nav-link-border-color)); - background:var(--sidebar-nav-link-background--hover, var(--sidebar-nav-link-background)); - color:var(--sidebar-nav-link-color--hover, var(--sidebar-nav-link-color)); - font-weight:var(--sidebar-nav-link-font-weight--hover, var(--sidebar-nav-link-font-weight)); - -webkit-text-decoration:var(--sidebar-nav-link-text-decoration--hover, var(--sidebar-nav-link-text-decoration)); - text-decoration:var(--sidebar-nav-link-text-decoration--hover, var(--sidebar-nav-link-text-decoration)); - -webkit-text-decoration-color:var(--sidebar-nav-link-text-decoration-color); - text-decoration-color:var(--sidebar-nav-link-text-decoration-color) -} -.sidebar-nav ul>li>span,.sidebar-nav ul>li>strong,.sidebar nav ul>li>span,.sidebar nav ul>li>strong{ - display:block; - margin:var(--sidebar-nav-strong-margin); - padding:var(--sidebar-nav-strong-padding); - border-width:var(--sidebar-nav-strong-border-width, 0); - border-style:solid; - border-color:var(--sidebar-nav-strong-border-color); - color:var(--sidebar-nav-strong-color); - font-size:var(--sidebar-nav-strong-font-size); - font-weight:var(--sidebar-nav-strong-font-weight); - text-transform:var(--sidebar-nav-strong-text-transform) -} -.sidebar-nav ul>li>span+ul,.sidebar-nav ul>li>strong+ul,.sidebar nav ul>li>span+ul,.sidebar nav ul>li>strong+ul{ - margin-left:0 -} -.sidebar-nav ul>li:first-child>span,.sidebar-nav ul>li:first-child>strong,.sidebar nav ul>li:first-child>span,.sidebar nav ul>li:first-child>strong{ - margin-top:0 -} -.sidebar-nav::-webkit-scrollbar,.sidebar nav::-webkit-scrollbar{ - width:0 -} -@supports(width: env(safe-area-inset)){ - @media only screen and (orientation: landscape){ - .sidebar-nav,.sidebar nav{ - margin-left:calc(env(safe-area-inset-left)/2) - } - } -} -.sidebar-nav li>a:before,.sidebar-nav li>strong:before{ - display:inline-block -} -.sidebar-nav li>a{ - background-repeat:var(--sidebar-nav-pagelink-background-repeat); - background-size:var(--sidebar-nav-pagelink-background-size) -} -.sidebar-nav li>a[href^="/"]:not([href*="?id="]),.sidebar-nav li>a[href^="#/"]:not([href*="?id="]){ - transition:var(--sidebar-nav-pagelink-transition) -} -.sidebar-nav li>a[href^="/"]:not([href*="?id="]),.sidebar-nav li>a[href^="/"]:not([href*="?id="])~ul a,.sidebar-nav li>a[href^="#/"]:not([href*="?id="]),.sidebar-nav li>a[href^="#/"]:not([href*="?id="])~ul a{ - padding:var(--sidebar-nav-pagelink-padding, var(--sidebar-nav-link-padding)) -} -.sidebar-nav li>a[href^="/"]:not([href*="?id="]):only-child,.sidebar-nav li>a[href^="#/"]:not([href*="?id="]):only-child{ - background:var(--sidebar-nav-pagelink-background) -} -.sidebar-nav li>a[href^="/"]:not([href*="?id="]):not(:only-child),.sidebar-nav li>a[href^="#/"]:not([href*="?id="]):not(:only-child){ - background:var(--sidebar-nav-pagelink-background--loaded, var(--sidebar-nav-pagelink-background)) -} -.sidebar-nav li.active>a,.sidebar-nav li.collapse>a{ - border-width:var(--sidebar-nav-link-border-width--active, var(--sidebar-nav-link-border-width)); - border-style:var(--sidebar-nav-link-border-style--active, var(--sidebar-nav-link-border-style)); - border-color:var(--sidebar-nav-link-border-color--active, var(--sidebar-nav-link-border-color)); - background:var(--sidebar-nav-link-background--active, var(--sidebar-nav-link-background)); - color:var(--sidebar-nav-link-color--active, var(--sidebar-nav-link-color)); - font-weight:var(--sidebar-nav-link-font-weight--active, var(--sidebar-nav-link-font-weight)); - -webkit-text-decoration:var(--sidebar-nav-link-text-decoration--active, var(--sidebar-nav-link-text-decoration)); - text-decoration:var(--sidebar-nav-link-text-decoration--active, var(--sidebar-nav-link-text-decoration)); - -webkit-text-decoration-color:var(--sidebar-nav-link-text-decoration-color); - text-decoration-color:var(--sidebar-nav-link-text-decoration-color) -} -.sidebar-nav li.active>a[href^="/"]:not([href*="?id="]):not(:only-child),.sidebar-nav li.active>a[href^="#/"]:not([href*="?id="]):not(:only-child){ - background:var(--sidebar-nav-pagelink-background--active, var(--sidebar-nav-pagelink-background--loaded, var(--sidebar-nav-pagelink-background))) -} -.sidebar-nav li.collapse>a[href^="/"]:not([href*="?id="]):not(:only-child),.sidebar-nav li.collapse>a[href^="#/"]:not([href*="?id="]):not(:only-child){ - background:var(--sidebar-nav-pagelink-background--collapse, var(--sidebar-nav-pagelink-background--loaded, var(--sidebar-nav-pagelink-background))) -} -.sidebar-nav li.collapse .app-sub-sidebar{ - display:none -} -.sidebar-nav>ul>li>a:before{ - content:var(--sidebar-nav-link-before-content-l1, var(--sidebar-nav-link-before-content)); - margin:var(--sidebar-nav-link-before-margin-l1, var(--sidebar-nav-link-before-margin)); - color:var(--sidebar-nav-link-before-color-l1, var(--sidebar-nav-link-before-color)) -} -.sidebar-nav>ul>li.active>a:before{ - content:var(--sidebar-nav-link-before-content-l1--active, var(--sidebar-nav-link-before-content--active, var(--sidebar-nav-link-before-content-l1, var(--sidebar-nav-link-before-content)))); - color:var(--sidebar-nav-link-before-color-l1--active, var(--sidebar-nav-link-before-color--active, var(--sidebar-nav-link-before-color-l1, var(--sidebar-nav-link-before-color)))) -} -.sidebar-nav>ul>li>ul>li>a:before{ - content:var(--sidebar-nav-link-before-content-l2, var(--sidebar-nav-link-before-content)); - margin:var(--sidebar-nav-link-before-margin-l2, var(--sidebar-nav-link-before-margin)); - color:var(--sidebar-nav-link-before-color-l2, var(--sidebar-nav-link-before-color)) -} -.sidebar-nav>ul>li>ul>li.active>a:before{ - content:var(--sidebar-nav-link-before-content-l2--active, var(--sidebar-nav-link-before-content--active, var(--sidebar-nav-link-before-content-l2, var(--sidebar-nav-link-before-content)))); - color:var(--sidebar-nav-link-before-color-l2--active, var(--sidebar-nav-link-before-color--active, var(--sidebar-nav-link-before-color-l2, var(--sidebar-nav-link-before-color)))) -} -.sidebar-nav>ul>li>ul>li>ul>li>a:before{ - content:var(--sidebar-nav-link-before-content-l3, var(--sidebar-nav-link-before-content)); - margin:var(--sidebar-nav-link-before-margin-l3, var(--sidebar-nav-link-before-margin)); - color:var(--sidebar-nav-link-before-color-l3, var(--sidebar-nav-link-before-color)) -} -.sidebar-nav>ul>li>ul>li>ul>li.active>a:before{ - content:var(--sidebar-nav-link-before-content-l3--active, var(--sidebar-nav-link-before-content--active, var(--sidebar-nav-link-before-content-l3, var(--sidebar-nav-link-before-content)))); - color:var(--sidebar-nav-link-before-color-l3--active, var(--sidebar-nav-link-before-color--active, var(--sidebar-nav-link-before-color-l3, var(--sidebar-nav-link-before-color)))) -} -.sidebar-nav>ul>li>ul>li>ul>li>ul>li>a:before{ - content:var(--sidebar-nav-link-before-content-l4, var(--sidebar-nav-link-before-content)); - margin:var(--sidebar-nav-link-before-margin-l4, var(--sidebar-nav-link-before-margin)); - color:var(--sidebar-nav-link-before-color-l4, var(--sidebar-nav-link-before-color)) -} -.sidebar-nav>ul>li>ul>li>ul>li>ul>li.active>a:before{ - content:var(--sidebar-nav-link-before-content-l4--active, var(--sidebar-nav-link-before-content--active, var(--sidebar-nav-link-before-content-l4, var(--sidebar-nav-link-before-content)))); - color:var(--sidebar-nav-link-before-color-l4--active, var(--sidebar-nav-link-before-color--active, var(--sidebar-nav-link-before-color-l4, var(--sidebar-nav-link-before-color)))) -} -.sidebar-nav>:last-child{ - margin-bottom:2rem -} -.sidebar-toggle,.sidebar-toggle-button{ - width:var(--sidebar-toggle-width); - outline:none -} -.sidebar-toggle{ - position:fixed; - z-index:11; - top:0; - bottom:0; - left:0; - max-width:40px; - margin:0; - padding:0; - border:0; - background:rgba(0,0,0,0); - -webkit-appearance:none; - -moz-appearance:none; - appearance:none; - cursor:pointer -} -.sidebar-toggle .sidebar-toggle-button{ - position:absolute; - top:var(--sidebar-toggle-offset-top); - left:var(--sidebar-toggle-offset-left); - height:var(--sidebar-toggle-height); - border-radius:var(--sidebar-toggle-border-radius); - border-width:var(--sidebar-toggle-border-width); - border-style:var(--sidebar-toggle-border-style); - border-color:var(--sidebar-toggle-border-color); - background:var(--sidebar-toggle-background, transparent); - color:var(--sidebar-toggle-icon-color) -} -.sidebar-toggle span{ - position:absolute; - top:calc(50% - var(--sidebar-toggle-icon-stroke-width)/2); - left:calc(50% - var(--sidebar-toggle-icon-width)/2); - height:var(--sidebar-toggle-icon-stroke-width); - width:var(--sidebar-toggle-icon-width); - background-color:currentColor -} -.sidebar-toggle span:nth-child(1){ - margin-top:calc(0px - var(--sidebar-toggle-icon-height)/2) -} -.sidebar-toggle span:nth-child(3){ - margin-top:calc(var(--sidebar-toggle-icon-height)/2) -} -@media(min-width: 48em){ - .sidebar-toggle{ - position:absolute; - overflow:visible; - top:var(--sidebar-toggle-offset-top); - bottom:auto; - left:0; - height:var(--sidebar-toggle-height); - transform:translateX(var(--sidebar-width)) - } - .sidebar-toggle .sidebar-toggle-button{ - top:0 - } -} -@media print{ - .sidebar-toggle{ - display:none - } -} -@media(max-width: 47.99em){ - body.close .sidebar,body.close .sidebar-toggle,body.close .sidebar+.content{ - transform:translateX(var(--sidebar-width)) - } -} -@media(min-width: 48em){ - body.close .sidebar+.content{ - transform:translateX(0) - } -} -@media(max-width: 47.99em){ - body.close nav.app-nav,body.close .github-corner{ - display:none - } -} -@media(min-width: 48em){ - body.close .sidebar,body.close .sidebar-toggle{ - transform:translateX(0) - } -} -@media(min-width: 48em){ - body.close nav.app-nav{ - margin-left:0 - } -} -@media(max-width: 47.99em){ - body.close .sidebar-toggle{ - width:100%; - max-width:none - } - body.close .sidebar-toggle span{ - margin-top:0 - } - body.close .sidebar-toggle span:nth-child(1){ - transform:rotate(45deg) - } - body.close .sidebar-toggle span:nth-child(2){ - display:none - } - body.close .sidebar-toggle span:nth-child(3){ - transform:rotate(-45deg) - } -} -@media(min-width: 48em){ - body.close .sidebar+.content{ - margin-left:0 - } -} -@media(min-width: 48em){ - body.sticky .sidebar,body.sticky .sidebar-toggle{ - position:fixed - } -} -body .docsify-copy-code-button,body .docsify-copy-code-button:after{ - border-radius:var(--border-radius-m, 0); - border-top-left-radius:0; - border-bottom-right-radius:0; - background:var(--copycode-background); - color:var(--copycode-color) -} -body .docsify-copy-code-button span{ - border-radius:var(--border-radius-s, 0) -} -body .docsify-pagination-container{ - border-top:var(--pagination-border-top); - color:var(--pagination-color) -} -body .pagination-item-label{ - font-size:var(--pagination-label-font-size) -} -body .pagination-item-label svg{ - color:var(--pagination-label-color); - height:var(--pagination-chevron-height); - stroke:var(--pagination-chevron-stroke); - stroke-linecap:var(--pagination-chevron-stroke-linecap); - stroke-linejoin:var(--pagination-chevron-stroke-linecap); - stroke-width:var(--pagination-chevron-stroke-width) -} -body .pagination-item-title{ - color:var(--pagination-title-color); - font-size:var(--pagination-title-font-size) -} -body .app-name.hide{ - display:block -} -body .sidebar{ - padding:var(--sidebar-padding) -} -.sidebar .search input{ - padding:0; - line-height:1; - font-size:inherit -} -.sidebar .search .clear-button svg{ - transform:scale(1) -} -.sidebar .search{ - order:var(--search-flex-order); - margin:var(--search-margin); - padding:var(--search-padding); - background:var(--search-background) -} -.sidebar .search a{ - color:inherit -} -.sidebar .search h2{ - margin:var(--search-result-heading-margin); - font-size:var(--search-result-heading-font-size); - font-weight:var(--search-result-heading-font-weight); - color:var(--search-result-heading-color) -} -.sidebar .search .input-wrap{ - align-items:stretch; - margin:var(--search-input-margin); - background-color:var(--search-input-background-color); - border-width:var(--search-input-border-width, 0); - border-style:solid; - border-color:var(--search-input-border-color); - border-radius:var(--search-input-border-radius) -} -.sidebar .search input[type=search]{ - min-width:0; - padding:var(--search-input-padding); - border:none; - background-color:rgba(0,0,0,0); - background-image:var(--search-input-background-image); - background-position:var(--search-input-background-position); - background-repeat:var(--search-input-background-repeat); - background-size:var(--search-input-background-size); - font-size:var(--search-input-font-size); - color:var(--search-input-color); - transition:var(--search-input-transition) -} -.sidebar .search input[type=search]::-ms-clear{ - display:none -} -.sidebar .search input[type=search]::-moz-placeholder{ - color:var(--search-input-placeholder-color, #808080) -} -.sidebar .search input[type=search]::placeholder{ - color:var(--search-input-placeholder-color, #808080) -} -.sidebar .search input[type=search]::-webkit-input-placeholder{ - line-height:normal -} -.sidebar .search input[type=search]:focus{ - background-color:var(--search-input-background-color--focus, var(--search-input-background-color)); - background-image:var(--search-input-background-image--focus, var(--search-input-background-image)); - background-position:var(--search-input-background-position--focus, var(--search-input-background-position)); - background-size:var(--search-input-background-size--focus, var(--search-input-background-size)) -} -@supports(width: env(safe-area-inset)){ - @media only screen and (orientation: landscape){ - .sidebar .search input[type=search]{ - margin-left:calc(env(safe-area-inset-left)/2) - } - } -} -.sidebar .search p{ - font-size:inherit; - overflow:hidden; - text-overflow:ellipsis; - -webkit-box-orient:vertical; - -webkit-line-clamp:2; - line-clamp:2 -} -.sidebar .search p:empty{ - text-align:center -} -.sidebar .search .clear-button{ - width:auto; - margin:0; - padding:0 10px; - border:none; - line-height:1; - background:rgba(0,0,0,0); - cursor:pointer -} -.sidebar .search .clear-button svg circle{ - fill:var(--search-clear-icon-color1, #808080) -} -.sidebar .search .clear-button svg path{ - stroke:var(--search-clear-icon-color2, #fff) -} -.sidebar .search.show~*:not(h1){ - display:none -} -.sidebar .search .results-panel{ - display:none; - color:var(--search-result-item-color); - font-size:var(--search-result-item-font-size); - font-weight:var(--search-result-item-font-weight) -} -.sidebar .search .results-panel.show{ - display:block -} -.sidebar .search .matching-post{ - border:none; - margin:var(--search-result-item-margin); - padding:var(--search-result-item-padding) -} -.sidebar .search .matching-post,.sidebar .search .matching-post:last-child{ - border-width:var(--search-result-item-border-width, 0) !important; - border-style:var(--search-result-item-border-style); - border-color:var(--search-result-item-border-color) -} -.sidebar .search .matching-post p{ - margin:0 -} -.sidebar .search .search-keyword{ - margin:var(--search-result-keyword-margin); - padding:var(--search-result-keyword-padding); - border-radius:var(--search-result-keyword-border-radius); - background-color:var(--search-result-keyword-background); - color:var(--search-result-keyword-color, currentColor); - font-style:normal; - font-weight:var(--search-result-keyword-font-weight) -} -.medium-zoom-overlay,.medium-zoom-image--open,.medium-zoom-image--opened{ - z-index:2147483646 !important -} -.medium-zoom-overlay{ - background:var(--zoomimage-overlay-background) !important -} diff --git a/lib/oscillators/_list.md b/lib/oscillators/_list.md index 418edeba..b49e2385 100644 --- a/lib/oscillators/_list.md +++ b/lib/oscillators/_list.md @@ -4,7 +4,7 @@ Done: 6, Todo: 23 ✔️ AC - Acceleration Oscillator ✔️ AO - Awesome Oscillator ✔️ *AROON - Aroon oscillator (Up, Down) -BOP - Balance of Power +, CCI - Commodity Channel Index CFO - Chande Forcast Oscillator ✔️ CMO - Chande Momentum Oscillator diff --git a/omnisharp.json b/omnisharp.json new file mode 100644 index 00000000..e907f421 --- /dev/null +++ b/omnisharp.json @@ -0,0 +1,19 @@ +{ + "RoslynExtensionsOptions": { + "enableAnalyzersSupport": true, + "enableDecompilationSupport": true + }, + "FormattingOptions": { + "enableEditorConfigSupport": true + }, + "Telemetry": { + "enableTelemetry": false + }, + "fileOptions": { + "systemExcludeSearchPatterns": [ + "**/node_modules/**/*", + "**/bin/**/*", + "**/obj/**/*" + ] + } +} From f55f630ac60b98f1edec8914cf006412348eec01 Mon Sep 17 00:00:00 2001 From: Miha Kralj <31756078+mihakralj@users.noreply.github.com> Date: Thu, 31 Oct 2024 11:19:40 -0700 Subject: [PATCH 13/17] Add CVI - Chaikin's Volatility class and tests Add implementation of CVI - Chaikin's Volatility class and related tests. * **Cvi Class Implementation:** - Add `Cvi` class in `lib/volatility/Cvi.cs` to calculate Chaikin's Volatility. - Use high and low prices for calculation. - Include a constructor with `period` parameter. - Add a method to calculate Chaikin's Volatility. * **Quantower Indicator:** - Add `CviIndicator` class in `quantower/Volatility/CviIndicator.cs`. - Use `Cvi` class for calculation. - Add input parameters for `period` and `showColdValues`. - Implement `OnInit` and `OnUpdate` methods. * **Tests:** - Add a test method for `Cvi` class in `Tests/test_updates_volatility.cs`. - Use random updates to test `Cvi`. - Ensure initial and final values are equal. --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/mihakralj/QuanTAlib?shareId=XXXX-XXXX-XXXX-XXXX). --- Tests/test_updates_volatility.cs | 16 ++++ lib/volatility/Cvi.cs | 109 +++++++++++++++++++++++++++ quantower/Volatility/CviIndicator.cs | 54 +++++++++++++ 3 files changed, 179 insertions(+) create mode 100644 lib/volatility/Cvi.cs create mode 100644 quantower/Volatility/CviIndicator.cs diff --git a/Tests/test_updates_volatility.cs b/Tests/test_updates_volatility.cs index f8877a8e..05d43c60 100644 --- a/Tests/test_updates_volatility.cs +++ b/Tests/test_updates_volatility.cs @@ -101,4 +101,20 @@ public class VolatilityUpdateTests Assert.Equal(initialValue, finalValue, precision); } + + [Fact] + public void Cvi_Update() + { + var indicator = new Cvi(period: 14); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } } diff --git a/lib/volatility/Cvi.cs b/lib/volatility/Cvi.cs new file mode 100644 index 00000000..a6895321 --- /dev/null +++ b/lib/volatility/Cvi.cs @@ -0,0 +1,109 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// CVI: Chaikin's Volatility +/// A technical indicator developed by Marc Chaikin that measures the volatility of a financial instrument by comparing the spread between the high and low prices. +/// +/// +/// The CVI calculation process: +/// 1. Calculates the difference between the high and low prices. +/// 2. Applies an exponential moving average (EMA) to the differences. +/// 3. Computes the percentage change in the EMA over a specified period. +/// +/// Key characteristics: +/// - Measures volatility +/// - Uses high and low prices +/// - Percentage-based +/// - EMA smoothing +/// +/// Formula: +/// CVI = (EMA(high - low, period) - EMA(high - low, period, offset)) / EMA(high - low, period, offset) * 100 +/// +/// Market Applications: +/// - Volatility assessment +/// - Trend confirmation +/// - Risk management +/// - Entry/exit timing +/// +/// Sources: +/// Marc Chaikin - Original development +/// https://www.investopedia.com/terms/c/chaikins-volatility.asp +/// +/// Note: Higher CVI values indicate higher volatility +/// + +[SkipLocalsInit] +public sealed class Cvi : AbstractBase +{ + private readonly int _period; + private readonly Ema _ema; + private readonly CircularBuffer _buffer; + private double _prevEma; + + /// The number of periods for CVI calculation. + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cvi(int period) + { + if (period < 1) + { + throw new ArgumentOutOfRangeException(nameof(period), + "Period must be greater than or equal to 1."); + } + _period = period; + _ema = new Ema(period); + _buffer = new CircularBuffer(period); + WarmupPeriod = period; + Name = $"CVI({period})"; + } + + /// The data source object that publishes updates. + /// The number of periods for CVI calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cvi(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _ema.Init(); + _buffer.Clear(); + _prevEma = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + double highLowDiff = BarInput.High - BarInput.Low; + _buffer.Add(highLowDiff, BarInput.IsNew); + + double ema = _ema.Calc(new TValue(Input.Time, highLowDiff, BarInput.IsNew)).Value; + + double cvi = 0; + if (_index >= _period) + { + double prevEma = _buffer[_buffer.Count - _period]; + cvi = (ema - prevEma) / prevEma * 100; + } + + _prevEma = ema; + IsHot = _index >= WarmupPeriod; + return cvi; + } +} diff --git a/quantower/Volatility/CviIndicator.cs b/quantower/Volatility/CviIndicator.cs new file mode 100644 index 00000000..7a43523f --- /dev/null +++ b/quantower/Volatility/CviIndicator.cs @@ -0,0 +1,54 @@ +using System.Drawing; +using TradingPlatform.BusinessLayer; + +namespace QuanTAlib; + +public class CviIndicator : Indicator, IWatchlistIndicator +{ + [InputParameter("Periods", sortIndex: 1, 1, 2000, 1, 0)] + public int Periods { get; set; } = 20; + + [InputParameter("Show cold values", sortIndex: 21)] + public bool ShowColdValues { get; set; } = true; + + private Cvi? cvi; + protected LineSeries? CviSeries; + public int MinHistoryDepths => Math.Max(5, Periods * 2); + int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths; + + public CviIndicator() + { + Name = "CVI - Chaikin's Volatility"; + Description = "Measures the volatility of a financial instrument by comparing the spread between the high and low prices."; + SeparateWindow = true; + + CviSeries = new($"CVI {Periods}", Color.Blue, 2, LineStyle.Solid); + AddLineSeries(CviSeries); + } + + protected override void OnInit() + { + cvi = new Cvi(Periods); + base.OnInit(); + } + + protected override void OnUpdate(UpdateArgs args) + { + TBar input = IndicatorExtensions.GetInputBar(this, args); + TValue result = cvi!.Calc(input); + + CviSeries!.SetValue(result.Value); + CviSeries!.SetMarker(0, Color.Transparent); //OnPaintChart draws the line, hidden here + } + +#pragma warning disable CA1416 // Validate platform compatibility + + public override string ShortName => $"CVI ({Periods})"; + + public override void OnPaintChart(PaintChartEventArgs args) + { + base.OnPaintChart(args); + this.PaintHLine(args, 0.05, new Pen(Color.DarkRed, width: 2)); + this.PaintSmoothCurve(args, CviSeries!, cvi!.WarmupPeriod, showColdValues: ShowColdValues, tension: 0.2); + } +} From 5b3f302ce42de2730772c2790c27405bb545edc0 Mon Sep 17 00:00:00 2001 From: Miha Kralj Date: Fri, 1 Nov 2024 17:38:50 -0700 Subject: [PATCH 14/17] Macd --- Tests/test_eventing.cs | 1 + Tests/test_updates_momentum.cs | 15 +++++ docs/indicators/indicators.md | 5 +- lib/momentum/Macd.cs | 114 +++++++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 lib/momentum/Macd.cs diff --git a/Tests/test_eventing.cs b/Tests/test_eventing.cs index 3564894e..45f49a3d 100644 --- a/Tests/test_eventing.cs +++ b/Tests/test_eventing.cs @@ -58,6 +58,7 @@ public class EventingTests ("Trima", new Trima(p), new Trima(input, p)), ("Vidya", new Vidya(p), new Vidya(input, p)), ("Apo", new Apo(12, 26), new Apo(input, 12, 26)), + ("Macd", new Macd(12, 26, 9), new Macd(input, 12, 26, 9)), ("Rsi", new Rsi(p), new Rsi(input, p)), ("Rsx", new Rsx(p), new Rsx(input, p)), ("Cmo", new Cmo(p), new Cmo(input, p)), diff --git a/Tests/test_updates_momentum.cs b/Tests/test_updates_momentum.cs index 7338cbdd..4f8c3028 100644 --- a/Tests/test_updates_momentum.cs +++ b/Tests/test_updates_momentum.cs @@ -120,6 +120,21 @@ public class MomentumUpdateTests Assert.Equal(initialValue, finalValue, precision); } + [Fact] + public void Macd_Update() + { + var indicator = new Macd(fastPeriod: 12, slowPeriod: 26, signalPeriod: 9); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble() + 100, IsNew: false)); // Ensure positive prices + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + [Fact] public void Pmo_Update() { diff --git a/docs/indicators/indicators.md b/docs/indicators/indicators.md index 35bd8401..abdc6468 100644 --- a/docs/indicators/indicators.md +++ b/docs/indicators/indicators.md @@ -14,7 +14,7 @@ - Total: 116 of 175 indicators implemented (66%) -|BASIC TRANSFORMS|Class Name| +|**BASIC TRANSFORMS**|**Class Name**| |---|:--:| |OC2 - Midpoint price|`.OC2`| |HL2 - Median Price|`.HL2`| @@ -22,8 +22,9 @@ |OHL3 - Mean Price|`.OHL3`| |OHLC4 - Average Price|`.OHLC4`| |HLCC4 - Weighted Price|`.HLCC4`| -
+ |**AVERAGES & TRENDS**|**Class Name**| +|--|:--:| |AFIRMA - Adaptive FIR Moving Average|`Afirma`| |ALMA - Arnaud Legoux Moving Average|`Alma`| |DEMA - Double Exponential Moving Average|`Dema`| diff --git a/lib/momentum/Macd.cs b/lib/momentum/Macd.cs new file mode 100644 index 00000000..eb8ad743 --- /dev/null +++ b/lib/momentum/Macd.cs @@ -0,0 +1,114 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// MACD: Moving Average Convergence Divergence +/// A trend-following momentum indicator that shows the relationship between two moving +/// averages of an asset's price. MACD is calculated by subtracting the longer-period +/// EMA from the shorter-period EMA. The result is then used to calculate a signal line +/// (EMA of MACD) and histogram (MACD - Signal). +/// +/// +/// The MACD calculation process: +/// 1. Calculate the fast EMA (default 12 periods) +/// 2. Calculate the slow EMA (default 26 periods) +/// 3. MACD Line = Fast EMA - Slow EMA +/// 4. Signal Line = EMA of MACD Line (default 9 periods) +/// 5. MACD Histogram = MACD Line - Signal Line +/// +/// Key characteristics: +/// - Centerline crossovers signal trend changes +/// - Signal line crossovers indicate trading opportunities +/// - Histogram shows momentum of price movement +/// - Divergences can signal potential reversals +/// +/// Formula: +/// MACD Line = EMA(fast) - EMA(slow) +/// Signal Line = EMA(MACD Line, signal) +/// Histogram = MACD Line - Signal Line +/// +/// Sources: +/// https://www.investopedia.com/terms/m/macd.asp +/// https://school.stockcharts.com/doku.php?id=technical_indicators:macd +/// + +[SkipLocalsInit] +public sealed class Macd : AbstractBase +{ + private readonly Ema _fastEma; + private readonly Ema _slowEma; + private readonly Ema _signalEma; + private const int DefaultFastPeriod = 12; + private const int DefaultSlowPeriod = 26; + private const int DefaultSignalPeriod = 9; + private double _macdLine; + private double _signalLine; + + /// + /// Gets the MACD line value (Fast EMA - Slow EMA) + /// + public double MacdLine => _macdLine; + + /// + /// Gets the Signal line value (EMA of MACD line) + /// + public double SignalLine => _signalLine; + + /// The number of periods for the fast EMA (default 12). + /// The number of periods for the slow EMA (default 26). + /// The number of periods for the signal line EMA (default 9). + /// Thrown when any period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Macd(int fastPeriod = DefaultFastPeriod, int slowPeriod = DefaultSlowPeriod, int signalPeriod = DefaultSignalPeriod) + { + if (fastPeriod < 1) + throw new ArgumentOutOfRangeException(nameof(fastPeriod)); + if (slowPeriod < 1) + throw new ArgumentOutOfRangeException(nameof(slowPeriod)); + if (signalPeriod < 1) + throw new ArgumentOutOfRangeException(nameof(signalPeriod)); + if (fastPeriod >= slowPeriod) + throw new ArgumentException("Fast period must be less than slow period"); + + _fastEma = new(fastPeriod); + _slowEma = new(slowPeriod); + _signalEma = new(signalPeriod); + WarmupPeriod = slowPeriod + signalPeriod; + Name = $"MACD({fastPeriod},{slowPeriod},{signalPeriod})"; + } + + /// The data source object that publishes updates. + /// The number of periods for the fast EMA. + /// The number of periods for the slow EMA. + /// The number of periods for the signal line EMA. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Macd(object source, int fastPeriod, int slowPeriod, int signalPeriod) : this(fastPeriod, slowPeriod, signalPeriod) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + _index++; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + // Calculate MACD line + double fastEma = _fastEma.Calc(Input.Value, Input.IsNew); + double slowEma = _slowEma.Calc(Input.Value, Input.IsNew); + _macdLine = fastEma - slowEma; + + // Calculate Signal line + _signalLine = _signalEma.Calc(_macdLine, Input.IsNew); + + // Return histogram + return _macdLine - _signalLine; + } +} From 7c6b698c9a2630dc0d5317f2f9bf15ec8fe5c0e1 Mon Sep 17 00:00:00 2001 From: Miha Kralj Date: Fri, 1 Nov 2024 17:54:03 -0700 Subject: [PATCH 15/17] Chop & Cog --- Tests/test_eventing.cs | 5 +- Tests/test_updates_oscillators.cs | 31 +++++++ docs/Progress.csv | 134 ------------------------------ docs/indicators/indicators.md | 16 ++-- lib/momentum/_list.md | 2 +- lib/oscillators/Chop.cs | 111 +++++++++++++++++++++++++ lib/oscillators/Cog.cs | 101 ++++++++++++++++++++++ lib/oscillators/_list.md | 12 +-- 8 files changed, 262 insertions(+), 150 deletions(-) delete mode 100644 docs/Progress.csv create mode 100644 lib/oscillators/Chop.cs create mode 100644 lib/oscillators/Cog.cs diff --git a/Tests/test_eventing.cs b/Tests/test_eventing.cs index 45f49a3d..78ad4f8e 100644 --- a/Tests/test_eventing.cs +++ b/Tests/test_eventing.cs @@ -62,6 +62,7 @@ public class EventingTests ("Rsi", new Rsi(p), new Rsi(input, p)), ("Rsx", new Rsx(p), new Rsx(input, p)), ("Cmo", new Cmo(p), new Cmo(input, p)), + ("Cog", new Cog(p), new Cog(input, p)), ("Curvature", new Curvature(p), new Curvature(input, p)), ("Entropy", new Entropy(p), new Entropy(input, p)), ("Kurtosis", new Kurtosis(p), new Kurtosis(input, p)), @@ -110,7 +111,9 @@ public class EventingTests ("Eom", new Eom(14), new Eom(barInput, 14)), ("Kvo", new Kvo(34, 55), new Kvo(barInput, 34, 55)), // Volatility indicators (bar-based) - ("Atr", new Atr(14), new Atr(barInput, 14)) + ("Atr", new Atr(14), new Atr(barInput, 14)), + // Oscillators (bar-based) + ("Chop", new Chop(14), new Chop(barInput, 14)) }; // Generate 200 random values and feed them to indicators diff --git a/Tests/test_updates_oscillators.cs b/Tests/test_updates_oscillators.cs index ba552e4e..a9d74a8e 100644 --- a/Tests/test_updates_oscillators.cs +++ b/Tests/test_updates_oscillators.cs @@ -156,4 +156,35 @@ public class OscillatorsUpdateTests Assert.Equal(initialValue, finalValue, precision); } + + [Fact] + public void Chop_Update() + { + var indicator = new Chop(period: 14); + TBar r = new(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TBar(DateTime.Now, GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), 1000, IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Cog_Update() + { + var indicator = new Cog(period: 10); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } } diff --git a/docs/Progress.csv b/docs/Progress.csv deleted file mode 100644 index 7ea5a50e..00000000 --- a/docs/Progress.csv +++ /dev/null @@ -1,134 +0,0 @@ -AC,Acceleration Oscillator -AD,Chaikin A/D Line -ADOSC,Chaikin A/D Oscillator -ADL,Accumulation / Distribution Line -ADX,Average Directional Movement Index -ADXR,Average Directional Movement Index Rating -Alligator,Alligator Indicator -ALMA,Arnaud Legoux Moving Average -AO,Awesome Oscillator -APO,Absolute Price Oscillator -AROON,Aroon Indicator -AROONOSC,Aroon Oscillator -ATS,ATR Trailing Stop -ATR,Average True Range -AVGPRICE,Average Price -BB,Bollinger Bands -BBF,Bollinger Bands Flat -BBP,Bull and Bear Power -BC,Beta Coefficient -BETA,Beta -BOP,Balance of Power -CCI,Commodity Channel Index -CE,Chandelier Exit -Channel,Price Channel -CI,Choppiness Index -CMF,Chaikin Money Flow -CMO,Chande Momentum Oscillator -CORREL,Pearson's Correlation Coefficient -CRSI,ConnorsRSI -DC,Donchian Channels -DCP,Dominant Cycle Periods -DEMA,Double Exponential Moving Average -DMI,Directional Movement Index -DPO,Detrended Price Oscillator -DX,Directional Movement Index -EMA,Exponential Moving Average -EPMA,Endpoint Moving Average -ERI,Elder-ray Index -FCB,Fractal Chaos Bands -FI,Force Index -GO,Gator Oscillator -HE,Hurst Exponent -HMA,Hull Moving Average -HT_TRENDLINE,Hilbert Transform - Instantaneous Trendline -HT_TRENDMODE,Hilbert Transform - Trend vs Cycle Mode -HV,Historical Volatility -IC,Ichimoku Cloud -KAMA,Kaufman Adaptive Moving Average -KDJ,KDJ Index -Keltner,Keltner Channel -KVO,Klinger Volume Oscillator -LINEARREG,Linear Regression -LINEARREG_ANGLE,Linear Regression Angle -LINEARREG_INTERCEPT,Linear Regression Intercept -LINEARREG_SLOPE,Linear Regression Slope -LSMA,Least Squares Moving Average -LWMA,Linearly Weighted Moving Average -MACDEXT,MACD with controllable MA type -MACDFIX,Moving Average Convergence Divergence Fix 12/26 -MAD,Mean absolute deviation -MAE,Moving Average Envelope -MAMA,MESA Adaptive Moving Average -MAPE,Mean absolute percentage error -MAX,Highest value over a specified period -MAXINDEX,Index of highest value over a specified period -MD,McGinley Dynamic -MEDPRICE,Median Price -MFI,Money Flow Index -MIN,Lowest value over a specified period -MININDEX,Index of lowest value over a specified period -MINMAX,Lowest and highest values over a specified period -MINMAXINDEX,Indexes of lowest and highest values over a period -MMA,Modified Moving Average -MOM,Momentum -MSE,Mean square error -NATR,Normalized Average True Range -OBV,On Balance Volume -OsMA,Moving Average of Oscillator -PAZ,Price Action Zones -Pivots,Pivots -PMO,Price Momentum Oscillator -PP,Pivot Points -PPO,Percentage Price Oscillator -PPMA,Pivot Point Moving Average -PRS,Price Relative Strength -PVI,Positive Volume Index -PVO,Percentage Volume Oscillator -Qstick,Qstick Indicator -R2,R-Squared (Coefficient of Determination) -Regression,Regression Line Indicator -RLW,%R Larry Williams -RMA,Running Moving Average -ROC,Rate of Change -ROCB,ROC with Bands -RPP,Rolling Pivot Points -RRA,Rescaled Range Analysis -RSI,Relative Strength Index -SAR,Parabolic SAR -SAREXT,Parabolic SAR - Extended -SDC,Standard Deviation Channels -SI,Swing Index -SLR,Slope and Linear Regression -SMA,Simple Moving Average -SMI,Stochastic Momentum Index -SMMA,Smoothed Moving Average -ST,SuperTrend -STARC,STARC Bands -STC,Schaff Trend Cycle -STDDEV,Standard Deviation -STOCH,Stochastic -STOCHF,Stochastic Fast -STOCHRSI,Stochastic Relative Strength Index -SUM,Summation -T3,Triple Exponential Moving Average (T3) -TEMA,Triple Exponential Moving Average -TRANGE,True Range -TRIMA,Triangular Moving Average -TRIX,1-day Rate-Of-Change (ROC) of a Triple Smooth EMA -TSI,True Strength Index -TYPPRICE,Typical Price -UI,Ulcer Index -ULTOSC,Ultimate Oscillator -VAR,Variance -VI,Vortex Indicator -Volume,Volume Indicator -VS,Volatility Stop -VWAP,Volume Weighted Average Price -VWMA,Volume Weighted Moving Average -WA,Williams Alligator -WCLPRICE,Weighted Close Price -WF,Williams Fractal -WMA,Weighted Moving Average -ZS,Z-Score -ZZ,ZigZag Indicator diff --git a/docs/indicators/indicators.md b/docs/indicators/indicators.md index abdc6468..81cfc570 100644 --- a/docs/indicators/indicators.md +++ b/docs/indicators/indicators.md @@ -5,13 +5,13 @@ **Implementation Status:** - Basic Transforms: 6 of 6 complete - Averages & Trends: 33 of 33 complete -- Momentum: 16 of 17 complete -- Oscillators: 6 of 29 complete +- Momentum: 17 of 17 complete +- Oscillators: 11 of 29 complete - Volatility: 11 of 35 complete - Volume: 15 of 19 complete - Numerical Analysis: 13 of 20 complete - Errors: 16 of 16 complete -- Total: 116 of 175 indicators implemented (66%) +- Total: 122 of 175 indicators implemented (70%) |**BASIC TRANSFORMS**|**Class Name**| @@ -67,7 +67,7 @@ |DMI* - Directional Movement Index (DI+, DI-)|`Dmi`| |DMX - Jurik Directional Movement Index|`Dmx`| |DPO - Detrended Price Oscillator|`Dpo`| -|🚧 MACD* - Moving Average Convergence/Divergence|`Macd`| +|MACD* - Moving Average Convergence/Divergence|`Macd`| |MOM - Momentum|`Mom`| |PMO - Price Momentum Oscillator|`Pmo`| |PO - Price Oscillator|`Po`| @@ -85,11 +85,11 @@ |AO - Awesome Oscillator|`Ao`| |AROON* - Aroon oscillator (Up, Down)|`Aroon`| |🚧 BOP - Balance of Power|`Bop`| -|🚧 CCI - Commodity Channel Index|`Cci`| -|🚧 CFO - Chande Forcast Oscillator|`Cfo`| +|CCI - Commodity Channel Index|`Cci`| +|CFO - Chande Forcast Oscillator|`Cfo`| |CMO - Chande Momentum Oscillator|`Cmo`| -|🚧 CHOP - Choppiness Index|`Chop`| -|🚧 COG - Ehler's Center of Gravity|`Cog`| +|CHOP - Choppiness Index|`Chop`| +|COG - Ehler's Center of Gravity|`Cog`| |🚧 COPPOCK - Coppock Curve|`Coppock`| |🚧 CRSI - Connor RSI|`Crsi`| |🚧 CTI - Ehler's Correlation Trend Indicator|`Cti`| diff --git a/lib/momentum/_list.md b/lib/momentum/_list.md index e141b879..1d01f144 100644 --- a/lib/momentum/_list.md +++ b/lib/momentum/_list.md @@ -7,7 +7,7 @@ Done: 15, Todo: 2 ✔️ *DMI - Directional Movement Index (DI+, DI-) ✔️ DMX - Jurik Directional Movement Index ✔️ DPO - Detrended Price Oscillator -*MACD - Moving Average Convergence/Divergence (MACD, Signal, Histogram) +✔️ *MACD - Moving Average Convergence/Divergence (MACD, Signal, Histogram) ✔️ MOM - Momentum ✔️ PMO - Price Momentum Oscillator ✔️ PO - Price Oscillator diff --git a/lib/oscillators/Chop.cs b/lib/oscillators/Chop.cs new file mode 100644 index 00000000..f58ef0c6 --- /dev/null +++ b/lib/oscillators/Chop.cs @@ -0,0 +1,111 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// CHOP: Choppiness Index +/// A technical indicator that measures the market's trendiness versus choppiness. +/// It helps determine if the market is trending or moving sideways by comparing +/// the total movement to the net directional movement over a period. +/// +/// +/// The CHOP calculation process: +/// 1. Calculate ATR sum over period +/// 2. Calculate total price range over period +/// 3. Scale result to oscillate between 0 and 100 +/// +/// Key characteristics: +/// - Oscillates between 0 and 100 +/// - Values above 61.8 indicate choppy market +/// - Values below 38.2 indicate trending market +/// - Based on ATR and price range +/// - Higher values = more choppy/sideways +/// - Lower values = more trending +/// +/// Formula: +/// CHOP = 100 * LOG10(SUM(ATR,n)/(HIGH(n)-LOW(n))) / LOG10(n) +/// where: +/// n = period +/// ATR = Average True Range +/// HIGH(n) = Highest high over period n +/// LOW(n) = Lowest low over period n +/// +/// Sources: +/// E.W. Dreiss +/// https://www.tradingview.com/support/solutions/43000501980-choppiness-index/ +/// +/// Note: Default period is 14 +/// + +[SkipLocalsInit] +public sealed class Chop : AbstractBase +{ + private readonly Atr _atr; + private readonly CircularBuffer _highs; + private readonly CircularBuffer _lows; + private readonly CircularBuffer _atrValues; + private readonly double _logPeriod; + private const int DefaultPeriod = 14; + private const double ScalingFactor = 100.0; + + /// The number of periods used in the CHOP calculation (default 14). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Chop(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + + _atr = new(period); + _highs = new(period); + _lows = new(period); + _atrValues = new(period); + _logPeriod = Math.Log10(period); + WarmupPeriod = period; + Name = $"CHOP({period})"; + } + + /// The data source object that publishes updates. + /// The number of periods used in the CHOP calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Chop(object source, int period = DefaultPeriod) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + _index++; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Calculate ATR and store it + double atr = _atr.Calc(BarInput); + _atrValues.Add(atr, BarInput.IsNew); + + // Store high and low prices + _highs.Add(BarInput.High, BarInput.IsNew); + _lows.Add(BarInput.Low, BarInput.IsNew); + + // Calculate highest high and lowest low over period + double highestHigh = _highs.Max(); + double lowestLow = _lows.Min(); + double range = highestHigh - lowestLow; + + // Calculate sum of ATR values + double atrSum = _atrValues.Sum(); + + // Avoid division by zero + if (range < double.Epsilon || _logPeriod < double.Epsilon) + return 0.0; + + // Calculate CHOP + return ScalingFactor * Math.Log10(atrSum / range) / _logPeriod; + } +} diff --git a/lib/oscillators/Cog.cs b/lib/oscillators/Cog.cs new file mode 100644 index 00000000..0c9aec23 --- /dev/null +++ b/lib/oscillators/Cog.cs @@ -0,0 +1,101 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// COG: Ehler's Center of Gravity Oscillator +/// A momentum oscillator that uses the concept of center of gravity from physics +/// to measure price momentum. It calculates a weighted sum where more recent +/// prices have higher weights. +/// +/// +/// The COG calculation process: +/// 1. Calculate weighted sum of prices (numerator) +/// 2. Calculate sum of weights (denominator) +/// 3. Divide to get center of gravity +/// 4. Invert and normalize result +/// +/// Key characteristics: +/// - Oscillates around zero +/// - Leading indicator (less lag than traditional momentum) +/// - Positive values indicate upward momentum +/// - Negative values indicate downward momentum +/// - Zero line crossovers signal trend changes +/// +/// Formula: +/// COG = -((Σ(Price(i) * i)) / (Σ(Price(i))) - (period + 1)/2) +/// where: +/// i = position in period (1 to period) +/// Price(i) = price at position i +/// +/// Sources: +/// John F. Ehlers - "Cybernetic Analysis for Stocks and Futures" +/// https://www.mesasoftware.com/papers/CenterOfGravity.pdf +/// +/// Note: Default period is 10 +/// + +[SkipLocalsInit] +public sealed class Cog : AbstractBase +{ + private readonly CircularBuffer _prices; + private readonly int _period; + private const int DefaultPeriod = 10; + + /// The number of periods used in the COG calculation (default 10). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cog(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + + _period = period; + _prices = new(period); + WarmupPeriod = period; + Name = $"COG({period})"; + } + + /// The data source object that publishes updates. + /// The number of periods used in the COG calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cog(object source, int period = DefaultPeriod) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + _index++; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + // Add new price to buffer + _prices.Add(Input.Value, Input.IsNew); + + double numerator = 0.0; + double denominator = 0.0; + + // Calculate weighted sums + for (int i = 0; i < _prices.Count; i++) + { + double price = _prices[i]; + double weight = i + 1; + numerator += price * weight; + denominator += price; + } + + // Avoid division by zero + if (Math.Abs(denominator) < double.Epsilon) + return 0.0; + + // Calculate center of gravity and normalize + return -((numerator / denominator) - (_period + 1.0) / 2.0); + } +} diff --git a/lib/oscillators/_list.md b/lib/oscillators/_list.md index b49e2385..e4cf7f2a 100644 --- a/lib/oscillators/_list.md +++ b/lib/oscillators/_list.md @@ -1,15 +1,15 @@ # Oscillators indicators -Done: 6, Todo: 23 +Done: 11, Todo: 18 ✔️ AC - Acceleration Oscillator ✔️ AO - Awesome Oscillator ✔️ *AROON - Aroon oscillator (Up, Down) -, -CCI - Commodity Channel Index -CFO - Chande Forcast Oscillator +BOP - Balance of Power +✔️ CCI - Commodity Channel Index +✔️ CFO - Chande Forcast Oscillator ✔️ CMO - Chande Momentum Oscillator -CHOP - Choppiness Index -COG - Ehler's Center of Gravity +✔️ CHOP - Choppiness Index +✔️ COG - Ehler's Center of Gravity COPPOCK - Coppock Curve CRSI - Connor RSI CTI - Ehler's Correlation Trend Indicator From f519594371e46607fda804d9faf5d5f3b9d13007 Mon Sep 17 00:00:00 2001 From: Miha Kralj Date: Fri, 1 Nov 2024 18:33:24 -0700 Subject: [PATCH 16/17] Adr, Ap, Atrp, Atrs, Vp, Vwap, Vwma --- docs/_sidebar.md | 12 +-- docs/index.html | 109 +++++++++++++++++++--- docs/indicators/indicators.md | 165 ++++++++++++++++------------------ lib/oscillators/_list.md | 2 +- lib/volatility/Adr.cs | 83 +++++++++++++++++ lib/volatility/Ap.cs | 120 +++++++++++++++++++++++++ lib/volatility/Atrp.cs | 85 ++++++++++++++++++ lib/volatility/Atrs.cs | 159 ++++++++++++++++++++++++++++++++ lib/volatility/_list.md | 10 +-- lib/volume/Vf.cs | 110 +++++++++++++++++++++++ lib/volume/Vp.cs | 105 ++++++++++++++++++++++ lib/volume/Vwap.cs | 93 +++++++++++++++++++ lib/volume/Vwma.cs | 92 +++++++++++++++++++ lib/volume/_list.md | 10 +-- 14 files changed, 1035 insertions(+), 120 deletions(-) create mode 100644 lib/volatility/Adr.cs create mode 100644 lib/volatility/Ap.cs create mode 100644 lib/volatility/Atrp.cs create mode 100644 lib/volatility/Atrs.cs create mode 100644 lib/volume/Vf.cs create mode 100644 lib/volume/Vp.cs create mode 100644 lib/volume/Vwap.cs create mode 100644 lib/volume/Vwma.cs diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 9c8f64e6..40084778 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -1,24 +1,24 @@ * [Home](/) -* [Available Indicators](indicators/indicators.md) +* [List of Indicators](indicators/indicators.md) -* Introduction +* 🚧 Introduction * [Overview]() * [Features]() * [Historical vs Real-time analysis](essays/realtime.md) -* Core Concepts +* 🚧 Core Concepts * [Time Series Data Handling]() * [Calculation classes]() * [Presentation Classes]() -* QuanTAlib C# Library +* 🚧 QuanTAlib C# Library * [Installation]() * [Quick Start Guide]() * [Usage Examples]() * [Tests and Validation]() -* Quantower Charts +* 🚧 Quantower Charts * [Installation]() * [Quick Start Guide]() * [Using VS Code for QuanTower coding](setup/vscode.md) @@ -26,7 +26,7 @@ * [Creating Custom Indicators]() * [Inspecting Quantower Internals]() -* [Available Indicators](indicators/indicators.md) +* [🚧 Available Indicators](indicators/indicators.md) * Momentum * [ADX - Average Directional Index](indicators/momentum/adx/description.md) * [ADXR - Average Directional Index Rating](indicators/momentum/adxr/description.md) diff --git a/docs/index.html b/docs/index.html index 00fb80ce..23a7f5e8 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3,57 +3,140 @@ QuanTAlib Documentation + + - - + + + + +
+ + - - - - - - - - - + + + + + + + + + + + + + + + + - + \ No newline at end of file diff --git a/docs/indicators/indicators.md b/docs/indicators/indicators.md index 81cfc570..4d97d32c 100644 --- a/docs/indicators/indicators.md +++ b/docs/indicators/indicators.md @@ -1,30 +1,27 @@ # Indicators in QuanTAlib -\* = Returns multiple values +| **Category** | **Status** | **Completion** | +|--------------|:----------:|:--------------:| +| Basic Transforms | 6 of 6 | 100% | +| Averages & Trends | 33 of 33 | 100% | +| Momentum | 17 of 17 | 100% | +| Oscillators | 12 of 29 | 41% | +| Volatility | 15 of 35 | 43% | +| Volume | 19 of 19 | 100% | +| Numerical Analysis | 13 of 20 | 65% | +| Errors | 16 of 16 | 100% | +| **Total** | **131 of 175** | **75%** | -**Implementation Status:** -- Basic Transforms: 6 of 6 complete -- Averages & Trends: 33 of 33 complete -- Momentum: 17 of 17 complete -- Oscillators: 11 of 29 complete -- Volatility: 11 of 35 complete -- Volume: 15 of 19 complete -- Numerical Analysis: 13 of 20 complete -- Errors: 16 of 16 complete -- Total: 122 of 175 indicators implemented (70%) - - -|**BASIC TRANSFORMS**|**Class Name**| -|---|:--:| +|Technical Indicator Name| Class Name| +|-----------|:----------:| +|**BASIC TRANSFORMS**|| |OC2 - Midpoint price|`.OC2`| |HL2 - Median Price|`.HL2`| |HLC3 - Typical Price|`.HLC3`| |OHL3 - Mean Price|`.OHL3`| |OHLC4 - Average Price|`.OHLC4`| |HLCC4 - Weighted Price|`.HLCC4`| - -|**AVERAGES & TRENDS**|**Class Name**| -|--|:--:| +|**AVERAGES & TRENDS**|| |AFIRMA - Adaptive FIR Moving Average|`Afirma`| |ALMA - Arnaud Legoux Moving Average|`Alma`| |DEMA - Double Exponential Moving Average|`Dema`| @@ -58,9 +55,7 @@ |VIDYA - Variable Index Dynamic Average|`Vidya`| |WMA - Weighted Moving Average|`Wma`| |ZLEMA - Zero-Lag Exponential Moving Average|`Zlema`| - -|**MOMENTUM INDICATORS**|**Class Name**| -|--|:--:| +|**MOMENTUM INDICATORS**|| |ADX - Average Directional Movement Index|`Adx`| |ADXR - Average Directional Movement Index Rating|`Adxr`| |APO - Absolute Price Oscillator|`Apo`| @@ -78,79 +73,73 @@ |TRIX - 1-day ROC of TEMA|`Trix`| |VEL - Jurik Signal Velocity|`Vel`| |VORTEX* - Vortex Indicator (VI+, VI-)|`Vortex`| - -|**OSCILLATORS**|**Class Name**| -|--|:--:| +|**OSCILLATORS**|| |AC - Acceleration Oscillator|`Ac`| |AO - Awesome Oscillator|`Ao`| |AROON* - Aroon oscillator (Up, Down)|`Aroon`| -|🚧 BOP - Balance of Power|`Bop`| +|BOP - Balance of Power|`Bop`| |CCI - Commodity Channel Index|`Cci`| |CFO - Chande Forcast Oscillator|`Cfo`| |CMO - Chande Momentum Oscillator|`Cmo`| |CHOP - Choppiness Index|`Chop`| |COG - Ehler's Center of Gravity|`Cog`| -|🚧 COPPOCK - Coppock Curve|`Coppock`| -|🚧 CRSI - Connor RSI|`Crsi`| -|🚧 CTI - Ehler's Correlation Trend Indicator|`Cti`| -|🚧 DOSC - Derivative Oscillator|`Dosc`| -|🚧 EFI - Elder Ray's Force Index|`Efi`| -|🚧 FISHER - Fisher Transform|`Fisher`| -|🚧 FOSC - Forecast Oscillator|`Fosc`| -|🚧 GATOR* - Williams Alliator Oscillator (Upper Jaw, Lower Jaw, Teeth)|`Gator`| -|🚧 KDJ* - KDJ Indicator (K, D, J lines)|`Kdj`| -|🚧 KRI - Kairi Relative Index|`Kri`| +|🚧 COPPOCK - Coppock Curve|`Coppock`| +|🚧 CRSI - Connor RSI|`Crsi`| +|🚧 CTI - Ehler's Correlation Trend Indicator|`Cti`| +|🚧 DOSC - Derivative Oscillator|`Dosc`| +|🚧 EFI - Elder Ray's Force Index|`Efi`| +|🚧 FISHER - Fisher Transform|`Fisher`| +|🚧 FOSC - Forecast Oscillator|`Fosc`| +|🚧 GATOR* - Williams Alliator Oscillator (Upper Jaw, Lower Jaw, Teeth)|`Gator`| +|🚧 KDJ* - KDJ Indicator (K, D, J lines)|`Kdj`| +|🚧 KRI - Kairi Relative Index|`Kri`| |RSI - Relative Strength Index|`Rsi`| |RSX - Jurik Trend Strength Index|`Rsx`| -|🚧 RVGI* - Relative Vigor Index (RVGI, Signal)|`Rvgi`| -|🚧 SMI - Stochastic Momentum Index|`Smi`| -|🚧 SRSI* - Stochastic RSI (SRSI, Signal)|`Srsi`| -|🚧 STC - Schaff Trend Cycle|`Stc`| -|🚧 STOCH* - Stochastic Oscillator (%K, %D)|`Stoch`| -|🚧 TSI - True Strength Index|`Tsi`| -|🚧 UO - Ultimate Oscillator|`Uo`| -|🚧 WILLR - Larry Williams' %R|`Willr`| - -|**VOLATILITY INDICATORS**|**Class Name**| -|--|:--:| -|🚧 ADR - Average Daily Range|`Adr`| -|🚧 AP - Andrew's Pitchfork|`Ap`| +|🚧 RVGI* - Relative Vigor Index (RVGI, Signal)|`Rvgi`| +|🚧 SMI - Stochastic Momentum Index|`Smi`| +|🚧 SRSI* - Stochastic RSI (SRSI, Signal)|`Srsi`| +|🚧 STC - Schaff Trend Cycle|`Stc`| +|🚧 STOCH* - Stochastic Oscillator (%K, %D)|`Stoch`| +|🚧 TSI - True Strength Index|`Tsi`| +|🚧 UO - Ultimate Oscillator|`Uo`| +|🚧 WILLR - Larry Williams' %R|`Willr`| +|**VOLATILITY INDICATORS**|| +|ADR - Average Daily Range|`Adr`| +|AP - Andrew's Pitchfork|`Ap`| |ATR - Average True Range|`Atr`| -|🚧 ATRP - Average True Range Percent|`Atrp`| -|🚧 ATRS - ATR Trailing Stop|`Atrs`| -|🚧 BB* - Bollinger Bands® (Upper, Middle, Lower)|`Bb`| -|🚧 CCV - Close-to-Close Volatility|`Ccv`| -|🚧 CE - Chandelier Exit|`Ce`| -|🚧 CV - Conditional Volatility (ARCH/GARCH)|`Cv`| -|🚧 CVI - Chaikin's Volatility|`Cvi`| -|🚧 DC* - Donchian Channels (Upper, Middle, Lower)|`Dc`| -|🚧 EWMA - Exponential Weighted Moving Average Volatility|`Ewma`| -|🚧 FCB - Fractal Chaos Bands|`Fcb`| -|🚧 GKV - Garman-Klass Volatility|`Gkv`| -|🚧 HLV - High-Low Volatility|`Hlv`| +|ATRP - Average True Range Percent|`Atrp`| +|ATRS - ATR Trailing Stop|`Atrs`| +|🚧 BB* - Bollinger Bands® (Upper, Middle, Lower)|`Bb`| +|🚧 CCV - Close-to-Close Volatility|`Ccv`| +|🚧 CE - Chandelier Exit|`Ce`| +|🚧 CV - Conditional Volatility (ARCH/GARCH)|`Cv`| +|🚧 CVI - Chaikin's Volatility|`Cvi`| +|🚧 DC* - Donchian Channels (Upper, Middle, Lower)|`Dc`| +|🚧 EWMA - Exponential Weighted Moving Average Volatility|`Ewma`| +|🚧 FCB - Fractal Chaos Bands|`Fcb`| +|🚧 GKV - Garman-Klass Volatility|`Gkv`| +|🚧 HLV - High-Low Volatility|`Hlv`| |HV - Historical Volatility|`Hv`| -|🚧 ICH* - Ichimoku Cloud (Conversion, Base, Leading Span A, Leading Span B, Lagging Span)|`Ich`| +|🚧 ICH* - Ichimoku Cloud (Conversion, Base, Leading Span A, Leading Span B, Lagging Span)|`Ich`| |JVOLTY - Jurik Volatility|`Jvolty`| -|🚧 KC* - Keltner Channels (Upper, Middle, Lower)|`Kc`| -|🚧 NATR - Normalized Average True Range|`Natr`| -|🚧 PCH - Price Channel Indicator|`Pch`| -|🚧 PSAR* - Parabolic Stop and Reverse (Value, Trend)|`Psar`| -|🚧 PV - Parkinson Volatility|`Pv`| -|🚧 RSV - Rogers-Satchell Volatility|`Rsv`| +|🚧 KC* - Keltner Channels (Upper, Middle, Lower)|`Kc`| +|🚧 NATR - Normalized Average True Range|`Natr`| +|🚧 PCH - Price Channel Indicator|`Pch`| +|🚧 PSAR* - Parabolic Stop and Reverse (Value, Trend)|`Psar`| +|🚧 PV - Parkinson Volatility|`Pv`| +|🚧 RSV - Rogers-Satchell Volatility|`Rsv`| |RV - Realized Volatility|`Rv`| |RVI - Relative Volatility Index|`Rvi`| -|🚧 STARC* - Starc Bands (Upper, Middle, Lower)|`Starc`| -|🚧 SV - Stochastic Volatility|`Sv`| +|🚧 STARC* - Starc Bands (Upper, Middle, Lower)|`Starc`| +|🚧 SV - Stochastic Volatility|`Sv`| |TR - True Range|`Tr`| |UI - Ulcer Index|`Ui`| |VC* - Volatility Cone (Mean, Upper Bound, Lower Bound)|`Vc`| |VOV - Volatility of Volatility|`Vov`| |VR - Volatility Ratio|`Vr`| |VS* - Volatility Stop (Long Stop, Short Stop)|`Vs`| -|🚧 YZV - Yang-Zhang Volatility|`Yzv`| - -|**VOLUME INDICATORS**|**Class Name**| -|--|:--:| +|🚧 YZV - Yang-Zhang Volatility|`Yzv`| +|**VOLUME INDICATORS**|| |ADL - Chaikin Accumulation Distribution Line|`Adl`| |ADOSC - Chaikin Accumulation Distribution Oscillator|`Adosc`| |AOBV - Archer On-Balance Volume|`Aobv`| @@ -166,36 +155,32 @@ |PVR - Price Volume Rank|`Pvr`| |PVT - Price Volume Trend|`Pvt`| |TVI - Trade Volume Index|`Tvi`| -|🚧 VF - Volume Force|`Vf`| -|🚧 VP - Volume Profile|`Vp`| -|🚧 VWAP - Volume Weighted Average Price|`Vwap`| -|🚧 VWMA - Volume Weighted Moving Average|`Vwma`| - -|**NUMERICAL ANALYSIS**|**Class Name**| -|--|:--:| -|🚧 BETA* - Beta coefficient (Beta, R-squared)|`Beta`| -|🚧 CORR* - Correlation Coefficient (Correlation, P-value)|`Corr`| +|VF - Volume Force|`Vf`| +|VP - Volume Profile|`Vp`| +|VWAP - Volume Weighted Average Price|`Vwap`| +|VWMA - Volume Weighted Moving Average|`Vwma`| +|**NUMERICAL ANALYSIS**|| +|🚧 BETA* - Beta coefficient (Beta, R-squared)|`Beta`| +|🚧 CORR* - Correlation Coefficient (Correlation, P-value)|`Corr`| |CURVATURE - Rate of Change in Direction or Slope|`Curvature`| |ENTROPY - Measure of Uncertainty or Disorder|`Entropy`| -|🚧 HUBER - Huber Loss|`Huber`| -|🚧 HURST - Hurst Exponent|`Hurst`| +|🚧 HUBER - Huber Loss|`Huber`| +|🚧 HURST - Hurst Exponent|`Hurst`| |KURTOSIS - Measure of Tails/Peakedness|`Kurtosis`| |MAX - Maximum with exponential decay|`Max`| |MEDIAN - Middle value|`Median`| |MIN - Minimum with exponential decay|`Min`| |MODE - Most Frequent Value|`Mode`| |PERCENTILE - Rank Order|`Percentile`| -|🚧 RSQUARED* - Coefficient of Determination (R-squared, Adjusted R-squared)|`Rsquared`| +|🚧 RSQUARED* - Coefficient of Determination (R-squared, Adjusted R-squared)|`Rsquared`| |SKEW - Skewness, asymmetry of distribution|`Skew`| |SLOPE - Rate of Change, Linear Regression|`Slope`| |STDDEV - Standard Deviation, Measure of Spread|`Stddev`| -|🚧 THEIL* - Theil's U Statistics (U1, U2)|`Theil`| -|🚧 TSF* - Time Series Forecast (Forecast, Confidence Interval)|`Tsf`| +|🚧 THEIL* - Theil's U Statistics (U1, U2)|`Theil`| +|🚧 TSF* - Time Series Forecast (Forecast, Confidence Interval)|`Tsf`| |VARIANCE - Average of Squared Deviations|`Variance`| |ZSCORE - Standardized Score|`Zscore`| - -|**ERRORS**|**Class Name**| -|--|:--:| +|**ERRORS**|| |HUBER - Huber Loss|`Huber`| |MAE - Mean Absolute Error|`Mae`| |MAPD - Mean Absolute Percentage Deviation|`Mapd`| diff --git a/lib/oscillators/_list.md b/lib/oscillators/_list.md index e4cf7f2a..718edbf5 100644 --- a/lib/oscillators/_list.md +++ b/lib/oscillators/_list.md @@ -4,7 +4,7 @@ Done: 11, Todo: 18 ✔️ AC - Acceleration Oscillator ✔️ AO - Awesome Oscillator ✔️ *AROON - Aroon oscillator (Up, Down) -BOP - Balance of Power +✔️ BOP - Balance of Power ✔️ CCI - Commodity Channel Index ✔️ CFO - Chande Forcast Oscillator ✔️ CMO - Chande Momentum Oscillator diff --git a/lib/volatility/Adr.cs b/lib/volatility/Adr.cs new file mode 100644 index 00000000..9e394dae --- /dev/null +++ b/lib/volatility/Adr.cs @@ -0,0 +1,83 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// ADR: Average Daily Range +/// A volatility indicator that measures the average range of price movement over +/// a specified period. It helps identify normal trading ranges and potential +/// breakout levels. +/// +/// +/// The ADR calculation process: +/// 1. Calculate daily range (High - Low) +/// 2. Apply SMA to daily ranges +/// 3. Updates with each new price bar +/// +/// Key characteristics: +/// - Simple volatility measure +/// - Period-based average +/// - Trend independent +/// - Absolute price measure +/// - Support/resistance aid +/// +/// Formula: +/// Daily Range = High - Low +/// ADR = SMA(Daily Range, period) +/// +/// Market Applications: +/// - Position sizing +/// - Volatility analysis +/// - Support/resistance levels +/// - Breakout identification +/// - Risk assessment +/// +/// Note: Simpler alternative to ATR, doesn't consider gaps +/// + +[SkipLocalsInit] +public sealed class Adr : AbstractBase +{ + private readonly Sma _ma; + private const int DefaultPeriod = 14; + + /// The number of periods for ADR calculation (default 14). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Adr(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + + _ma = new(period); + WarmupPeriod = period; + Name = $"ADR({period})"; + } + + /// The data source object that publishes updates. + /// The number of periods for ADR calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Adr(object source, int period = DefaultPeriod) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + _index++; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Calculate daily range + double range = BarInput.High - BarInput.Low; + + // Apply SMA smoothing + return _ma.Calc(range, BarInput.IsNew); + } +} diff --git a/lib/volatility/Ap.cs b/lib/volatility/Ap.cs new file mode 100644 index 00000000..5ce827fb --- /dev/null +++ b/lib/volatility/Ap.cs @@ -0,0 +1,120 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// AP: Andrew's Pitchfork +/// A trend channel tool that uses three points to create a channel with a median +/// line and two parallel lines. It helps identify potential support and resistance +/// levels based on market pivots. +/// +/// +/// The AP calculation process: +/// 1. Use three pivot points (P0, P1, P2) +/// 2. Calculate median line from P0 to midpoint of P1-P2 +/// 3. Draw parallel lines at P1 and P2 +/// 4. Project all lines forward +/// +/// Key characteristics: +/// - Trend channel tool +/// - Support/resistance levels +/// - Price projection +/// - Market geometry +/// - Pivot-based analysis +/// +/// Formula: +/// Median Line = Line from P0 to (P1 + P2)/2 +/// Upper Line = Parallel to median at P1 +/// Lower Line = Parallel to median at P2 +/// +/// Market Applications: +/// - Trend analysis +/// - Support/resistance +/// - Price targets +/// - Channel trading +/// - Market structure +/// +/// Sources: +/// Dr. Alan Andrews +/// https://www.investopedia.com/terms/a/andrewspitchfork.asp +/// +/// Note: Returns median line value for current price level +/// + +[SkipLocalsInit] +public sealed class Ap : AbstractBase +{ + private readonly CircularBuffer _highs; + private readonly CircularBuffer _lows; + private readonly CircularBuffer _closes; + private const int DefaultPeriod = 20; + + /// The lookback period for pivot points (default 20). + /// Thrown when period is less than 3. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Ap(int period = DefaultPeriod) + { + if (period < 3) + throw new ArgumentOutOfRangeException(nameof(period)); + + _highs = new(period); + _lows = new(period); + _closes = new(period); + WarmupPeriod = period; + Name = $"AP({period})"; + } + + /// The data source object that publishes updates. + /// The lookback period for pivot points. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Ap(object source, int period = DefaultPeriod) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + _index++; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double x, double y) FindPivot(CircularBuffer highs, CircularBuffer lows, CircularBuffer closes, int offset) + { + double high = highs[offset]; + double low = lows[offset]; + double close = closes[offset]; + return (offset, (high + low + close) / 3.0); // Simple pivot point calculation + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Store price data + _highs.Add(BarInput.High, BarInput.IsNew); + _lows.Add(BarInput.Low, BarInput.IsNew); + _closes.Add(BarInput.Close, BarInput.IsNew); + + if (_index < WarmupPeriod) + return BarInput.Close; + + // Find three pivot points + var p0 = FindPivot(_highs, _lows, _closes, 2); + var p1 = FindPivot(_highs, _lows, _closes, 1); + var p2 = FindPivot(_highs, _lows, _closes, 0); + + // Calculate midpoint of P1-P2 + double midX = (p1.x + p2.x) / 2.0; + double midY = (p1.y + p2.y) / 2.0; + + // Calculate slope of median line + double slope = (midY - p0.y) / (midX - p0.x); + + // Project median line to current bar + double currentX = _index - p0.x; + return p0.y + slope * currentX; + } +} diff --git a/lib/volatility/Atrp.cs b/lib/volatility/Atrp.cs new file mode 100644 index 00000000..a5546e77 --- /dev/null +++ b/lib/volatility/Atrp.cs @@ -0,0 +1,85 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// ATRP: Average True Range Percent +/// A volatility indicator that expresses ATR as a percentage of current price. +/// This normalization allows for comparison across different price levels and +/// instruments. +/// +/// +/// The ATRP calculation process: +/// 1. Calculate ATR normally +/// 2. Divide by current price +/// 3. Multiply by 100 for percentage +/// +/// Key characteristics: +/// - Normalized volatility measure +/// - Price-independent comparison +/// - Percentage output +/// - Cross-market analysis +/// - Relative volatility measure +/// +/// Formula: +/// ATRP = (ATR / Close) * 100 +/// +/// Market Applications: +/// - Cross-market comparison +/// - Position sizing +/// - Volatility analysis +/// - Risk assessment +/// - Market comparison +/// +/// Note: More suitable for comparing different instruments than raw ATR +/// + +[SkipLocalsInit] +public sealed class Atrp : AbstractBase +{ + private readonly Atr _atr; + private const int DefaultPeriod = 14; + private const double ScalingFactor = 100.0; + + /// The number of periods for ATR calculation (default 14). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Atrp(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + + _atr = new(period); + WarmupPeriod = period; + Name = $"ATRP({period})"; + } + + /// The data source object that publishes updates. + /// The number of periods for ATR calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Atrp(object source, int period = DefaultPeriod) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + _index++; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Calculate ATR + double atr = _atr.Calc(BarInput); + + // Convert to percentage of price + return Math.Abs(BarInput.Close) > double.Epsilon + ? (atr / BarInput.Close) * ScalingFactor + : 0.0; + } +} diff --git a/lib/volatility/Atrs.cs b/lib/volatility/Atrs.cs new file mode 100644 index 00000000..803cdb0f --- /dev/null +++ b/lib/volatility/Atrs.cs @@ -0,0 +1,159 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// ATRS: ATR Trailing Stop +/// A volatility-based trailing stop indicator that uses ATR to dynamically adjust +/// stop levels. It helps maintain position while allowing for normal market +/// fluctuations. +/// +/// +/// The ATRS calculation process: +/// 1. Calculate ATR +/// 2. Multiply ATR by factor +/// 3. Apply trailing logic based on trend +/// 4. Update stop levels +/// +/// Key characteristics: +/// - Dynamic stop levels +/// - Trend-following +/// - Volatility-based +/// - Position protection +/// - Risk management +/// +/// Formula: +/// Long Stop = High - (ATR * Factor) +/// Short Stop = Low + (ATR * Factor) +/// where Factor is multiplier for ATR (default 2.0) +/// +/// Market Applications: +/// - Stop loss placement +/// - Position management +/// - Trend following +/// - Risk control +/// - Exit strategy +/// +/// Note: Returns stop level based on current trend +/// + +[SkipLocalsInit] +public sealed class Atrs : AbstractBase +{ + private readonly Atr _atr; + private double _prevStop; + private double _p_prevStop; + private bool _isLong; + private bool _p_isLong; + private const int DefaultPeriod = 14; + private const double DefaultFactor = 2.0; + + /// + /// Gets the current trend direction (true for long, false for short) + /// + public bool IsLong => _isLong; + + /// The number of periods for ATR calculation (default 14). + /// The multiplier for ATR (default 2.0). + /// Thrown when period is less than 1 or factor is less than or equal to 0. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Atrs(int period = DefaultPeriod, double factor = DefaultFactor) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + if (factor <= 0) + throw new ArgumentOutOfRangeException(nameof(factor)); + + _atr = new(period); + Factor = factor; + WarmupPeriod = period; + Name = $"ATRS({period},{factor:F1})"; + } + + /// The data source object that publishes updates. + /// The number of periods for ATR calculation. + /// The multiplier for ATR. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Atrs(object source, int period = DefaultPeriod, double factor = DefaultFactor) : this(period, factor) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + /// + /// Gets or sets the ATR multiplier factor + /// + public double Factor { get; set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _atr.Init(); + _prevStop = double.NaN; + _isLong = true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _index++; + _p_prevStop = _prevStop; + _p_isLong = _isLong; + } + else + { + _prevStop = _p_prevStop; + _isLong = _p_isLong; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Calculate ATR + double atr = _atr.Calc(BarInput); + double atrBand = atr * Factor; + + if (_index == 1 || double.IsNaN(_prevStop)) + { + // Initialize stop level + _isLong = BarInput.Close > BarInput.Open; + _prevStop = _isLong ? BarInput.Low - atrBand : BarInput.High + atrBand; + return _prevStop; + } + + // Update stop level based on trend + if (_isLong) + { + double newStop = BarInput.High - atrBand; + if (BarInput.Close < _prevStop) + { + _isLong = false; + _prevStop = BarInput.High + atrBand; + } + else if (newStop > _prevStop) + { + _prevStop = newStop; + } + } + else + { + double newStop = BarInput.Low + atrBand; + if (BarInput.Close > _prevStop) + { + _isLong = true; + _prevStop = BarInput.Low - atrBand; + } + else if (newStop < _prevStop) + { + _prevStop = newStop; + } + } + + return _prevStop; + } +} diff --git a/lib/volatility/_list.md b/lib/volatility/_list.md index 65243f2c..49854141 100644 --- a/lib/volatility/_list.md +++ b/lib/volatility/_list.md @@ -1,11 +1,11 @@ # Volatility indicators -Done: 11, Todo: 24 +Done: 15, Todo: 20 -ADR - Average Daily Range -AP - Andrew's Pitchfork +✔️ ADR - Average Daily Range +✔️ AP - Andrew's Pitchfork ✔️ ATR - Average True Range -ATRP - Average True Range Percent -ATRS - ATR Trailing Stop +✔️ ATRP - Average True Range Percent +✔️ ATRS - ATR Trailing Stop *BB - Bollinger Bands® (Upper, Middle, Lower) CCV - Close-to-Close Volatility CE - Chandelier Exit diff --git a/lib/volume/Vf.cs b/lib/volume/Vf.cs new file mode 100644 index 00000000..f987bfd1 --- /dev/null +++ b/lib/volume/Vf.cs @@ -0,0 +1,110 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// VF: Volume Force +/// A volume-based indicator that measures the strength of volume relative to price +/// movement. It helps identify whether volume is supporting or contradicting the +/// current price trend. +/// +/// +/// The VF calculation process: +/// 1. Calculate price change +/// 2. Calculate volume force as volume * price change +/// 3. Optionally smooth the result with EMA +/// +/// Key characteristics: +/// - Volume-weighted measure +/// - Trend strength indicator +/// - No upper/lower bounds +/// - Raw and smoothed versions +/// - Divergence indicator +/// +/// Formula: +/// VF = Volume * (Close - Close[1]) +/// Smoothed VF = EMA(VF, period) +/// +/// Market Applications: +/// - Volume analysis +/// - Trend confirmation +/// - Price/volume divergence +/// - Market participation +/// - Momentum confirmation +/// +/// Note: Higher values indicate stronger volume force +/// + +[SkipLocalsInit] +public sealed class Vf : AbstractBase +{ + private readonly Ema _ema; + private double _prevClose; + private double _p_prevClose; + private const int DefaultPeriod = 13; + + /// The smoothing period for EMA calculation (default 13). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vf(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + + _ema = new(period); + WarmupPeriod = period + 1; + Name = $"VF({period})"; + } + + /// The data source object that publishes updates. + /// The smoothing period for EMA calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vf(object source, int period = DefaultPeriod) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _ema.Init(); + _prevClose = double.NaN; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _index++; + _p_prevClose = _prevClose; + } + else + { + _prevClose = _p_prevClose; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + if (_index == 1) + { + _prevClose = BarInput.Close; + return 0; + } + + // Calculate raw volume force + double priceChange = BarInput.Close - _prevClose; + double volumeForce = BarInput.Volume * priceChange; + + // Update previous close + _prevClose = BarInput.Close; + + // Apply EMA smoothing + return _ema.Calc(volumeForce, BarInput.IsNew); + } +} diff --git a/lib/volume/Vp.cs b/lib/volume/Vp.cs new file mode 100644 index 00000000..d923f588 --- /dev/null +++ b/lib/volume/Vp.cs @@ -0,0 +1,105 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// VP: Volume Profile +/// A volume-based indicator that analyzes volume distribution across price levels. +/// It helps identify significant price levels where most trading activity occurs. +/// +/// +/// The VP calculation process: +/// 1. Track volume at each price level within a period +/// 2. Calculate Point of Control (POC) - price with highest volume +/// 3. Calculate Value Area (70% of total volume) +/// +/// Key characteristics: +/// - Price level analysis +/// - Volume distribution +/// - Support/resistance identification +/// - Trading activity concentration +/// - Market structure analysis +/// +/// Formula: +/// VP = Σ Volume at each price level +/// POC = Price level with max volume +/// Value Area = Price range containing 70% of volume +/// +/// Market Applications: +/// - Support/resistance levels +/// - Market structure analysis +/// - Trading activity patterns +/// - Price level significance +/// - Volume concentration +/// +/// Note: Returns Point of Control (price level with highest volume) +/// + +[SkipLocalsInit] +public sealed class Vp : AbstractBase +{ + private readonly CircularBuffer _volumes; + private readonly CircularBuffer _prices; + private const int DefaultPeriod = 14; + + /// The number of periods to analyze volume distribution (default 14). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vp(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + + _volumes = new(period); + _prices = new(period); + WarmupPeriod = period; + Name = $"VP({period})"; + } + + /// The data source object that publishes updates. + /// The number of periods to analyze volume distribution. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vp(object source, int period = DefaultPeriod) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + _index++; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static int FindMaxVolumeIndex(CircularBuffer volumes) + { + int maxIndex = 0; + double maxVolume = volumes[0]; + + for (int i = 1; i < volumes.Count; i++) + { + if (volumes[i] > maxVolume) + { + maxVolume = volumes[i]; + maxIndex = i; + } + } + + return maxIndex; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Store volume and price + _volumes.Add(BarInput.Volume, BarInput.IsNew); + _prices.Add(BarInput.Close, BarInput.IsNew); + + // Find price level with highest volume (Point of Control) + int pocIndex = FindMaxVolumeIndex(_volumes); + return _prices[pocIndex]; + } +} diff --git a/lib/volume/Vwap.cs b/lib/volume/Vwap.cs new file mode 100644 index 00000000..b07a64b5 --- /dev/null +++ b/lib/volume/Vwap.cs @@ -0,0 +1,93 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// VWAP: Volume Weighted Average Price +/// A trading benchmark that shows the ratio of the value traded to total volume +/// traded over a specific period. VWAP equals the dollar value of all trading +/// periods divided by the total trading volume for the current day. +/// +/// +/// The VWAP calculation process: +/// 1. Calculate typical price for each period +/// 2. Multiply typical price by volume +/// 3. Calculate cumulative values +/// 4. Divide cumulative (price * volume) by cumulative volume +/// +/// Key characteristics: +/// - Intraday trading benchmark +/// - Volume-weighted measure +/// - Institutional trading reference +/// - Price momentum indicator +/// - Trading efficiency measure +/// +/// Formula: +/// VWAP = Σ(Price * Volume) / ΣVolume +/// where Price = (High + Low + Close)/3 +/// +/// Market Applications: +/// - Best execution analysis +/// - Trading algorithms +/// - Price momentum +/// - Market impact analysis +/// - Order timing +/// +/// Sources: +/// https://www.investopedia.com/terms/v/vwap.asp +/// +/// Note: Commonly used by institutional traders +/// + +[SkipLocalsInit] +public sealed class Vwap : AbstractBase +{ + private double _cumulativeTPV; // Cumulative (Typical Price * Volume) + private double _cumulativeVolume; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vwap() + { + WarmupPeriod = 1; + Name = "VWAP"; + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vwap(object source) : this() + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _cumulativeTPV = 0; + _cumulativeVolume = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + _index++; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Update cumulative values only for new bars + if (BarInput.IsNew) + { + _cumulativeTPV += BarInput.HLC3 * BarInput.Volume; + _cumulativeVolume += BarInput.Volume; + } + + // Calculate VWAP + return _cumulativeVolume > 0 ? _cumulativeTPV / _cumulativeVolume : BarInput.HLC3; + } +} diff --git a/lib/volume/Vwma.cs b/lib/volume/Vwma.cs new file mode 100644 index 00000000..3c1892c9 --- /dev/null +++ b/lib/volume/Vwma.cs @@ -0,0 +1,92 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// VWMA: Volume Weighted Moving Average +/// A technical indicator that combines price and volume to show the average price +/// weighted by volume over a period. It gives more weight to prices with higher +/// volume, making it more responsive to high-volume price movements. +/// +/// +/// The VWMA calculation process: +/// 1. Multiply price by volume for each period +/// 2. Sum (price * volume) over the period +/// 3. Sum volume over the period +/// 4. Divide sums to get weighted average +/// +/// Key characteristics: +/// - Volume-sensitive average +/// - Trend indicator +/// - Support/resistance levels +/// - Price momentum +/// - Volume emphasis +/// +/// Formula: +/// VWMA = Σ(Price * Volume) / ΣVolume +/// where sums are taken over the specified period +/// +/// Market Applications: +/// - Trend identification +/// - Support/resistance levels +/// - Volume analysis +/// - Price momentum +/// - Trading signals +/// +/// Note: More responsive to high-volume price movements +/// + +[SkipLocalsInit] +public sealed class Vwma : AbstractBase +{ + private readonly CircularBuffer _priceVolume; + private readonly CircularBuffer _volume; + private const int DefaultPeriod = 20; + + /// The number of periods for VWMA calculation (default 20). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vwma(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + + _priceVolume = new(period); + _volume = new(period); + WarmupPeriod = period; + Name = $"VWMA({period})"; + } + + /// The data source object that publishes updates. + /// The number of periods for VWMA calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vwma(object source, int period = DefaultPeriod) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + _index++; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Calculate and store price * volume + double priceVolume = BarInput.Close * BarInput.Volume; + _priceVolume.Add(priceVolume, BarInput.IsNew); + _volume.Add(BarInput.Volume, BarInput.IsNew); + + // Calculate sums + double sumPriceVolume = _priceVolume.Sum(); + double sumVolume = _volume.Sum(); + + // Calculate VWMA + return sumVolume > 0 ? sumPriceVolume / sumVolume : BarInput.Close; + } +} diff --git a/lib/volume/_list.md b/lib/volume/_list.md index 8fdb2f14..dce6b41d 100644 --- a/lib/volume/_list.md +++ b/lib/volume/_list.md @@ -1,5 +1,5 @@ # Volume indicators -Done: 15, Todo: 3 +Done: 19, Todo: 0 ✔️ ADL - Chaikin Accumulation Distribution Line ✔️ ADOSC - Chaikin Accumulation Distribution Oscillator @@ -16,7 +16,7 @@ Done: 15, Todo: 3 ✔️ PVR - Price Volume Rank ✔️ PVT - Price Volume Trend ✔️ TVI - Trade Volume Index -VF - Volume Force -VP - Volume Profile -VWAP - Volume Weighted Average Price -VWMA - Volume Weighted Moving Average +✔️ VF - Volume Force +✔️ VP - Volume Profile +✔️ VWAP - Volume Weighted Average Price +✔️ VWMA - Volume Weighted Moving Average From 3d648e59e3a5972862983a712f5efd24a252d8d5 Mon Sep 17 00:00:00 2001 From: Miha Kralj Date: Fri, 1 Nov 2024 18:54:48 -0700 Subject: [PATCH 17/17] improved github actions workflow --- .github/workflows/Publish.yml | 60 ++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/.github/workflows/Publish.yml b/.github/workflows/Publish.yml index 9fb5e4f9..bab332b0 100644 --- a/.github/workflows/Publish.yml +++ b/.github/workflows/Publish.yml @@ -12,7 +12,7 @@ permissions: jobs: Code_Coverage: - runs-on: ubuntu-latest + runs-on: windows-latest steps: - name: Checkout repository uses: actions/checkout@v4 @@ -24,6 +24,20 @@ jobs: with: dotnet-version: '8.x' + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: 'zulu' + + - name: Cache SonarCloud scanner + id: cache-sonar-scanner + uses: actions/cache@v4 + with: + path: .\.sonar\scanner + key: ${{ runner.os }}-sonar-scanner + restore-keys: ${{ runner.os }}-sonar-scanner + - name: Install dotnet tools run: | dotnet tool install JetBrains.dotCover.GlobalTool --global @@ -33,6 +47,26 @@ jobs: dotnet tool install --global dotnet-reportgenerator-globaltool dotnet restore + - name: Begin SonarCloud Analysis + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + shell: powershell + run: | + dotnet sonarscanner begin /k:"mihakralj_QuanTAlib" /o:"mihakralj-quantalib" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" ` + /d:sonar.solution.file="QuanTAlib.sln" ` + /d:sonar.cs.opencover.reportsPaths="**/*cover*.xml" ` + /d:sonar.cs.dotcover.reportsPaths="**/dotcover.xml" ` + /d:sonar.coverage.exclusions="**Tests.cs,**/*.md,**/*.html,**/*.css,**/docs/**/*,**/archive/**/*,**/notebooks/**/*,**/obj/**/*,**/bin/**/*" ` + /d:sonar.exclusions="**/TestResults/**/*,**/bin/**/*,**/obj/**/*,**/*.html,**/coverage/**/*,**/CoverageReport/**/*,**/*.md,**/*.css,**/docs/**/*,**/archive/**/*,**/notebooks/**/*" ` + /d:sonar.test.exclusions="**Tests.cs,**/obj/**/*,**/bin/**/*" ` + /d:sonar.cpd.exclusions="**Tests.cs" ` + /d:sonar.cs.roslyn.ignoreIssues="false" ` + /d:sonar.issue.ignore.multicriteria="e1" ` + /d:sonar.issue.ignore.multicriteria.e1.ruleKey="csharpsquid:S1944,csharpsquid:S2053,csharpsquid:S2222,csharpsquid:S2259,csharpsquid:S2583,csharpsquid:S2589,csharpsquid:S3329,csharpsquid:S3655,csharpsquid:S3900,csharpsquid:S3949,csharpsquid:S3966,csharpsquid:S4158,csharpsquid:S4347,csharpsquid:S5773,csharpsquid:S6781" ` + /d:sonar.issue.ignore.multicriteria.e1.resourceKey="**/*.cs" ` + /d:sonar.verbose="true" + - name: Build Projects run: | dotnet build --no-restore --configuration Debug @@ -54,6 +88,13 @@ jobs: run: | reportgenerator -reports:*cover*.xml -targetdir:. + - name: End SonarCloud Analysis + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + shell: powershell + run: dotnet sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" + - name: Upload Coverage to Codacy uses: codacy/codacy-coverage-reporter-action@v1 with: @@ -66,23 +107,6 @@ jobs: files: 'cover*' verbose: true - - name: SonarCloud Scan - uses: SonarSource/sonarcloud-github-action@master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - with: - args: > - -Dsonar.projectKey=mihakralj_QuanTAlib - -Dsonar.organization=mihakralj-quantalib - -Dsonar.sources=. - -Dsonar.cs.opencover.reportsPaths=**/*cover*.xml - -Dsonar.cs.dotcover.reportsPaths=**/dotcover.xml - -Dsonar.coverage.exclusions=**Tests.cs,**/*.md,**/*.html,**/*.css,**/docs/**/*,**/archive/**/*,**/notebooks/**/* - -Dsonar.exclusions=**/TestResults/**/*,**/bin/**/*,**/obj/**/*,**/*.html,**/coverage/**/*,**/CoverageReport/**/*,**/*.md,**/*.css,**/docs/**/*,**/archive/**/*,**/notebooks/**/* - -Dsonar.cpd.exclusions=**Tests.cs - -Dsonar.test.exclusions=**Tests.cs - CodeQL: runs-on: ubuntu-latest steps: