diff --git a/GitVersion.yml b/GitVersion.yml index 1eba6ce9..ce39df9f 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -1,6 +1,6 @@ +mode: ContinuousDeployment assembly-versioning-scheme: MajorMinorPatch assembly-file-versioning-scheme: MajorMinorPatch -mode: ContinuousDeployment tag-prefix: '[vV]?' major-version-bump-message: '\+semver:\s?(breaking|major)' minor-version-bump-message: '\+semver:\s?(feature|minor)' @@ -34,4 +34,4 @@ branches: pre-release-weight: 0 ignore: - sha: [] \ No newline at end of file + sha: [] diff --git a/Tests/test_Trady.cs b/Tests/test_Trady.cs index 00bacbab..ebf70c4e 100644 --- a/Tests/test_Trady.cs +++ b/Tests/test_Trady.cs @@ -11,7 +11,6 @@ public class TradyTests private readonly Random rnd; private readonly double range; private readonly int iterations; - private int period; private readonly int skip; private readonly IEnumerable Candles; @@ -39,7 +38,7 @@ public class TradyTests { for (int run = 0; run < iterations; run++) { - period = rnd.Next(50) + 5; + int period = rnd.Next(50) + 5; Sma ma = new(period); TSeries QL = new(); foreach (TBar item in feed) @@ -69,7 +68,7 @@ public class TradyTests { for (int run = 0; run < iterations; run++) { - period = rnd.Next(50) + 5; + int period = rnd.Next(50) + 5; Ema ma = new(period); TSeries QL = new(); foreach (TBar item in feed) diff --git a/Tests/test_Tulip.cs b/Tests/test_Tulip.cs index 4729e61b..174b4046 100644 --- a/Tests/test_Tulip.cs +++ b/Tests/test_Tulip.cs @@ -8,7 +8,6 @@ public class TulipTests private readonly GbmFeed feed; private readonly Random rnd; private readonly double range; - private int period; private readonly int iterations; private readonly double[] data; private readonly double[] outdata; @@ -32,7 +31,7 @@ public class TulipTests { for (int run = 0; run < iterations; run++) { - period = rnd.Next(50) + 5; + int period = rnd.Next(50) + 5; Sma ma = new(period); TSeries QL = new(); foreach (TBar item in feed) @@ -56,8 +55,7 @@ public class TulipTests { for (int run = 0; run < iterations; run++) { - period = rnd.Next(50) + 5; - period = 20; + int period = rnd.Next(30) + 5; Ema ma = new(period, useSma: false); TSeries QL = new(); foreach (TBar item in feed) diff --git a/Tests/test_talib.cs b/Tests/test_talib.cs index cd073b8e..dcd64c32 100644 --- a/Tests/test_talib.cs +++ b/Tests/test_talib.cs @@ -8,8 +8,7 @@ public class TAlibTests private readonly GbmFeed feed; private readonly Random rnd; private readonly double range; - private int period; - private readonly int iterations; + private readonly int iterations; private readonly double[] data; private readonly double[] TALIB; @@ -32,7 +31,7 @@ public class TAlibTests { for (int run = 0; run < iterations; run++) { - period = rnd.Next(50) + 5; + int period = rnd.Next(50) + 5; Sma ma = new(period); TSeries QL = new(); foreach (TBar item in feed) @@ -52,7 +51,7 @@ public class TAlibTests { for (int run = 0; run < iterations; run++) { - period = rnd.Next(50) + 5; + int period = rnd.Next(50) + 5; Ema ma = new(period, useSma: true); TSeries QL = new(); foreach (TBar item in feed) @@ -72,7 +71,7 @@ public class TAlibTests { for (int run = 0; run < iterations; run++) { - period = rnd.Next(50) + 5; + int period = rnd.Next(50) + 5; Dema ma = new(period); TSeries QL = new(); foreach (TBar item in feed) @@ -92,7 +91,7 @@ public class TAlibTests { for (int run = 0; run < iterations; run++) { - period = rnd.Next(50) + 5; + int period = rnd.Next(50) + 5; Tema ma = new(period); TSeries QL = new(); foreach (TBar item in feed) @@ -135,7 +134,7 @@ public class TAlibTests { for (int run = 0; run < iterations; run++) { - period = rnd.Next(50) + 5; + int period = rnd.Next(50) + 5; T3 ma = new(period, vfactor: 0.7, useSma: false); TSeries QL = new(); foreach (TBar item in feed) diff --git a/docs/Progress.md b/docs/Progress.md index 5847f8dc..793f4881 100644 --- a/docs/Progress.md +++ b/docs/Progress.md @@ -1,5 +1,5 @@ # Backlog and done -|**QT**|**Cht**|Cmnt|Docs|isNew|Valid| +|**QT**|**Chart**|Cmnt|Docs|isNew|Validation| |--|:--:|:--:|:--:|:--:|:--:| |AFIRMA|✔️||||| \ No newline at end of file diff --git a/docs/indicators/indicators.md b/docs/indicators/indicators.md index e26a8a65..d11d04e8 100644 --- a/docs/indicators/indicators.md +++ b/docs/indicators/indicators.md @@ -6,13 +6,47 @@ |**BASIC TRANSFORMS**|**QuanTALib**|Skender.Stock|TALib.NETCore|Tulip.NETCore|Trady| |--|:--:|:--:|:--:|:--:|:--:| -|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`|| +|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|| |
|||| +|**STATISTICS AND NUMERICAL ANALYSIS**|**QuanTALib**|Skender.Stock|TALib.NETCore|Tulip.NETCore|Trady| +|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||||| +|MAX - Maximum with exponential decay|`Max`|||| +|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||||| +|MEDIAN - Middle value|`Median`|||| +|MIN - Minimum with exponential decay|`Min`|||| +|MODE - Most Frequent Value|`Mode`|||| +|MPE - Pean Percentage Error||||| +|MSE - Mean Squared Error||||| +|MSLE - Mean Squared Logarithmic Error||||| +|PERCENTILE - Rank Order|`Percentile`|||| +|RSQUARED - Coefficient of Determination R-Squared||||| +|RAE - Relative Absolute Error||||| +|RMSE - Root Mean Squared Error||||| +|RSE - Relateive Squared Error||||| +|RMSLE - Root Mean Squared Logarithmic Error||||| +|SKEW - Skewness, asymmetry of distribution|`Skew`|||| +|SLOPE - Rate of Change, Linear Regression|`Slope`|||| +|SMAPE - Symmetric Mean Absolute Percentage Error||||| +|STDDEV - Standard Deviation, Measure of Spread||||| +|THEIL - Theil's U Statistics||||| +|VARIANCE - Average of Squared Deviations|`Variance`|||| +|ZSCORE - Standardized Score|`Zscore`|||| +|
||||| |**AVERAGES & TRENDS**|**QuanTALib**|Skender.Stock|TALib.NETCore|Tulip.NETCore|Trady| |AFIRMA - Autoregressive Finite Impulse Response Moving Average|`Afirma`|||| |ALMA - Arnaud Legoux Moving Average|`Alma`|`✔️`||| @@ -57,84 +91,83 @@ |ZLEMA - Zero Lag EMA Average|`Zlema`|||`✔️`| |
|||| |**VOLATILITY INDICATORS**|**QuanTALib**|Skender.Stock|TALib.NETCore|Tulip.NETCore|Trady| -|ADL - Chaikin Accumulation Distribution Line||`GetAdl`|`Ad`|| -|ADOSC - Chaikin Accumulation Distribution Oscillator||`GetChaikinOsc`|`AdOsc`|| -|ATR - Average True Range||`GetAtr`|`Atr`|| +|ADL - Chaikin Accumulation Distribution Line||GetAdl|Ad|| +|ADOSC - Chaikin Accumulation Distribution Oscillator||GetChaikinOsc|AdOsc|| +|ATR - Average True Range||GetAtr|Atr|| |ATRP - Average True Range Percent||||| -|ATRSTOP - ATR Trailing Stop ||`GetAtrStop`||| -|BETA - Beta coefficient||||| -|BBANDS - Bollinger Bands®||`BollingerBands`||| -|CHAND - Chandelier Exit||`GetChandelier`||| -|CRSI - Connor RSI||`GetConnorsRsi`||| +|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`||| +|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`||| +|ICH - Ichimoku Cloud||GetIchimoku||| +|KEL - Keltner Channels||GetKeltner||| +|NATR - Normalized Average True Range||GetAtr||| |CHN - Price Channel Indicator||||| -|RSI - Relative Strength Index||`GetRsi`||| -|SAR - Parabolic Stop and Reverse||`GetParabolicSar`||| -|SRSI - Stochastic RSI||`GetStochRsi`||| -|STARC - Starc Bands||`GetStarcBands`||| +|RSI - Relative Strength Index||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`||| +|UI - Ulcer Index||GetUlcerIndex||| +|VSTOP - Volatility Stop||GetVolatilityStop||| |
|||| |**MOMENTUM INDICATORS & OSCILLATORS**|**QuanTALib**|Skender.Stock|TALib.NETCore|Tulip.NETCore|Trady| |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`|| +|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`||| +|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`||| +|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`||| +|GATOR - Gator oscillator||GetGator||| +|HURST - Hurst Exponent||GetHurst||| |KRI - Kairi Relative Index||||| -|KVO - Klinger Volume Oscillator||`GetKvo`|||| -|MFI - Money Flow Index||`GetMfi`||| +|KVO - Klinger Volume Oscillator||GetKvo|||| +|MFI - Money Flow Index||GetMfi||| |MOM - Momentum||||| |NVI - Negative Volume Index||||| |PO - Price Oscillator||||| |PPO - Percentage Price Oscillator||||| -|PMO - Price Momentum Oscillator||`GetPmo`||| +|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||GetWillia`msR`||| -|WGAT - Williams Alligator||`GetAlligator`||| +|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|Tulip.NETCore|Trady| |AOBV - Archer On-Balance Volume||||| -|CMF - Chaikin Money Flow||`GetCmf`||| +|CMF - Chaikin Money Flow||GetCmf||| |EOM - Ease of Movement||||| |KVO - Klinger Volume Oscilaltor||||| -|OBV - On-Balance Volume||`GetObv`||| -|PRS - Price Relative Strength||`GetPrs`||| +|OBV - On-Balance Volume||GetObv||| +|PRS - Price Relative Strength||`GetPrs||| |PVOL - Price-Volume||||| -|PVO - Percentage Volume Oscillator||`GetPvo`||| +|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`|||| +|VWAP - Volume Weighted Average Price||GetVwap||| +|VWMA - Volume Weighted Moving Average||GetVwma|||| diff --git a/docs/indicators/volatility/rvi/calc.md b/docs/indicators/volatility/rvi/calc.md new file mode 100644 index 00000000..eeb283a4 --- /dev/null +++ b/docs/indicators/volatility/rvi/calc.md @@ -0,0 +1,50 @@ +# The Math Behind RVI + +## Components of RVI + +The **Relative Volatility Index (RVI)** measures the direction of volatility in the market, using components like: + +- Standard deviation of price changes +- Simple moving average (SMA) to smooth volatility +- Separation of up and down price movements + +### RVI Formula + +The RVI is calculated using the following formula: + +$$ +\text{RVI}_t = 100 \times \frac{\text{SMA}(\sigma_{\text{up}}, N)}{\text{SMA}(\sigma_{\text{up}}, N) + \text{SMA}(\sigma_{\text{down}}, N)} +$$ + +Where: +- \( \text{RVI}_t \) is the RVI value at time \( t \) +- \( \sigma_{\text{up}} \) is the standard deviation of up moves over the lookback period \( N \) +- \( \sigma_{\text{down}} \) is the standard deviation of down moves over the lookback period \( N \) +- \( \text{SMA} \) represents the simple moving average applied over \( N \) periods + +### Up and Down Move Calculation + +The standard deviations \( \sigma_{\text{up}} \) and \( \sigma_{\text{down}} \) are calculated based on the price changes: + +$$ +\Delta \text{Price} = \text{Close}_t - \text{Close}_{t-1} +$$ + +- If \( \Delta \text{Price} > 0 \), it contributes to \( \sigma_{\text{up}} \) +- If \( \Delta \text{Price} < 0 \), it contributes to \( \sigma_{\text{down}} \) + +### Parameter Definitions + +RVI uses the following main parameters: + +- **Lookback period** (\( N \)): The number of periods used to calculate the standard deviations and SMAs. A typical value is 14. +- **Smoothing with SMA**: The standard deviations of up and down moves are smoothed using a simple moving average (SMA), making the RVI less sensitive to short-term fluctuations. + +### Computational Process + +For each new data point: +- Calculate the price change (\( \Delta \text{Price} \)) from the previous period. +- Separate the price changes into up moves and down moves. +- Compute the standard deviations (\( \sigma_{\text{up}} \) and \( \sigma_{\text{down}} \)) over the last \( N \) periods. +- Apply the simple moving average (SMA) to both up and down standard deviations. +- Use the RVI formula to produce the final RVI value. diff --git a/docs/indicators/volatility/rvi/rvi.md b/docs/indicators/volatility/rvi/rvi.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/readme.md b/docs/readme.md index aa548a91..f3ee745e 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -27,9 +27,10 @@ ## Installation to Quantower -- `` is the directory where Quantower is installed - where `Start.lnk` launcher is +- `` is the directory where Quantower is installed - where `Start.lnk` launcher is. Copy any or all `dll` files as below: - Copy `Averages.dll` from Releases to `\Settings\Scripts\Indicators\Averages\Averages.dll` - Copy `Statistics.dll` from Releases to `\Settings\Scripts\Indicators\Statistics\Statistics.dll` +- Copy `Volatility.dll` from Releases to `\Settings\Scripts\Indicators\Volatility\Volatility.dll` - Copy `SyntheticVendor.dll` from Releases to `\Settings\Scripts\Vendors\SyntheticVendor\SyntheticVendor.dll` diff --git a/lib/core/AbstractBarBase.cs b/lib/core/AbstractBarBase.cs index cd643345..9a4de1cf 100644 --- a/lib/core/AbstractBarBase.cs +++ b/lib/core/AbstractBarBase.cs @@ -1,56 +1,76 @@ namespace QuanTAlib; /// -/// Provides a base implementation for financial indicators in the QuanTAlib library. -/// This abstract class implements the iTValue interface and defines common properties -/// and methods used by inheriting indicator types. +/// Provides a base implementation for financial indicators that work with bar data in the QuanTAlib library. /// -public abstract class AbstractBarBase : iTValue -{ +/// +/// This abstract class implements the iTValue interface and defines common properties +/// and methods used by inheriting indicator types. It handles the basic flow of +/// receiving bar data, performing calculations, and publishing results. +/// +public abstract class AbstractBarBase : iTValue { public 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 int WarmupPeriod { get; set; } - - public TValue Tick => new(Time, Value, IsNew, IsHot); // Stores the current value of indicator - public event ValueSignal Pub = delegate { }; // Publisher of generated values - - protected int _index; //tracking the position of output + public TValue Tick => new(Time, Value, IsNew, IsHot); + public event ValueSignal Pub = delegate { }; + protected int _index; protected double _lastValidValue; - // other _internal vars defined here - - protected AbstractBarBase() - { //add parameters into constructor + protected AbstractBarBase() { + // Add parameters into constructor if needed } + /// + /// Subscribes to bar data updates. + /// + /// The source of the bar data. + /// The event arguments containing the bar data. public void Sub(object source, in TBarEventArgs args) => Calc(args.Bar); - public virtual void Init() - { + /// + /// Initializes the indicator's state. + /// + public virtual void Init() { _index = 0; _lastValidValue = 0; } - public virtual TValue Calc(TBar input) - { + /// + /// Calculates the indicator value based on the input bar. + /// + /// The input bar data. + /// A TValue containing the calculated result. + public virtual TValue Calc(TBar input) { Input = input; - if (double.IsNaN(input.Close) || double.IsInfinity(input.Close)) - { + if (double.IsNaN(input.Close) || double.IsInfinity(input.Close)) { return Process(new TValue(Time: input.Time, Value: GetLastValid(), IsNew: input.IsNew, IsHot: true)); } this.Value = Calculation(); return Process(new TValue(Time: Input.Time, Value: this.Value, IsNew: Input.IsNew, IsHot: this.IsHot)); } - protected virtual double GetLastValid() - { + /// + /// Retrieves the last valid calculated value. + /// + /// The last valid value of the indicator. + protected virtual double GetLastValid() { return this.Value; } + + /// + /// Manages the state of the indicator based on whether a new bar is being processed. + /// + /// Indicates whether the current input is a new bar. protected abstract void ManageState(bool isNew); + + /// + /// Performs the actual calculation of the indicator value. + /// + /// The calculated indicator value. protected abstract double Calculation(); /// @@ -59,8 +79,7 @@ public abstract class AbstractBarBase : iTValue /// /// The calculated TValue to process. /// The processed TValue. - protected virtual TValue Process(TValue value) - { + protected virtual TValue Process(TValue value) { this.Time = value.Time; this.Value = value.Value; this.IsNew = value.IsNew; diff --git a/lib/core/abstractBase.cs b/lib/core/abstractBase.cs index 84dfc144..ee6ea0cb 100644 --- a/lib/core/abstractBase.cs +++ b/lib/core/abstractBase.cs @@ -2,29 +2,29 @@ namespace QuanTAlib; /// /// Provides a base implementation for financial indicators in the QuanTAlib library. -/// This abstract class implements the iTValue interface and defines common properties -/// and methods used by inheriting indicator types. /// +/// +/// This abstract class implements the iTValue interface and defines common properties +/// and methods used by inheriting indicator types. It handles the basic flow of +/// receiving data, performing calculations, and publishing results. +/// public abstract class AbstractBase : iTValue { public DateTime Time { get; set; } public double Value { get; set; } public bool IsNew { get; set; } public bool IsHot { get; set; } - public TValue Input { get; set; } public String Name { get; set; } = ""; public int WarmupPeriod { get; set; } - - public TValue Tick => new(Time, Value, IsNew, IsHot); // Stores the current value of indicator - public event ValueSignal Pub = delegate { }; // Publisher of generated values - - protected int _index; //tracking the position of output + public TValue Tick => new(Time, Value, IsNew, IsHot); + public event ValueSignal Pub = delegate { }; + protected int _index; protected double _lastValidValue; - // other _internal vars defined here protected AbstractBase() - { //add parameters into constructor + { + // Add parameters into constructor if needed } /// @@ -34,6 +34,9 @@ public abstract class AbstractBase : iTValue /// The argument containing the new data point. public void Sub(object source, in ValueEventArgs args) => Calc(args.Tick); + /// + /// Initializes the indicator's state. + /// public virtual void Init() { _index = 0; @@ -41,11 +44,14 @@ public abstract class AbstractBase : iTValue } /// - /// Calculates the indicator value based on the input; calls specific Calculation() method - /// where implementation is + /// Calculates the indicator value based on the input. /// /// The input value for the calculation. /// A TValue representing the calculated indicator value. + /// + /// This method calls the specific Calculation() method where the actual implementation is. + /// If the input value is NaN or infinity, it returns the last valid value instead. + /// public virtual TValue Calc(TValue input) { Input = input; @@ -57,11 +63,25 @@ public abstract class AbstractBase : iTValue return Process(new TValue(Time: Input.Time, Value: this.Value, IsNew: Input.IsNew, IsHot: this.IsHot)); } + /// + /// Retrieves the last valid calculated value. + /// + /// The last valid value of the indicator. protected virtual double GetLastValid() { return this.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 695c4da3..04af52a5 100644 --- a/lib/core/circularbuffer.cs +++ b/lib/core/circularbuffer.cs @@ -4,55 +4,72 @@ using System.Numerics; namespace QuanTAlib; -public class CircularBuffer : IEnumerable -{ +/// +/// Represents a circular buffer of double values with fixed capacity. +/// +/// +/// This class provides efficient operations for adding, accessing, and manipulating +/// a fixed-size buffer of double values. It uses SIMD operations for improved performance +/// on supported hardware. +/// +public class CircularBuffer : IEnumerable { private readonly double[] _buffer; 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; } + + /// + /// Gets the number of elements currently contained in the buffer. + /// public int Count => _size; - public CircularBuffer(int capacity) - { + /// + /// Initializes a new instance of the CircularBuffer class with the specified capacity. + /// + /// The maximum number of elements the buffer can hold. + public CircularBuffer(int capacity) { Capacity = capacity; _buffer = GC.AllocateArray(capacity, pinned: true); } + /// + /// Adds an item to the buffer. + /// + /// The item to add to the buffer. + /// Indicates whether the item is a new value or an update to the last added value. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Add(double item, bool isNew = true) - { - if (_size == 0 || isNew) - { - if (_size < Capacity) - { + public void Add(double item, bool isNew = true) { + if (_size == 0 || isNew) { + if (_size < Capacity) { _buffer[(_start + _size) % Capacity] = item; _size++; - } - else - { + } else { _buffer[_start] = item; _start = (_start + 1) % Capacity; } - } - else - { + } else { _buffer[(_start + _size - 1) % Capacity] = item; } } - public double this[Index index] - { + /// + /// Gets or sets the element at the specified index. + /// + /// The zero-based index of the element to get or set. + /// The element at the specified index. + public double this[Index index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { + get { int actualIndex = index.IsFromEnd ? _size - index.Value : index.Value; actualIndex = Math.Clamp(actualIndex, 0, _size - 1); return _buffer[(_start + actualIndex) % Capacity]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - set - { + set { int actualIndex = index.IsFromEnd ? _size - index.Value : index.Value; actualIndex = Math.Clamp(actualIndex, 0, _size - 1); _buffer[(_start + actualIndex) % Capacity] = value; @@ -60,54 +77,66 @@ public class CircularBuffer : IEnumerable } [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowArgumentOutOfRangeException() - { + private static void ThrowArgumentOutOfRangeException() { throw new ArgumentOutOfRangeException("index", "Index is out of range."); } + /// + /// Gets the newest (most recently added) element in the buffer. + /// + /// The newest element in the buffer. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public double Newest() - { + public double Newest() { if (_size == 0) return 0; return _buffer[(_start + _size - 1) % Capacity]; } + /// + /// Gets the oldest element in the buffer. + /// + /// The oldest element in the buffer. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public double Oldest() - { + public double Oldest() { if (_size == 0) ThrowInvalidOperationException(); return _buffer[_start]; } [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowInvalidOperationException() - { + private static void ThrowInvalidOperationException() { throw new InvalidOperationException("Buffer is empty."); } + /// + /// Returns an enumerator that iterates through the buffer. + /// + /// An enumerator for the buffer. public Enumerator GetEnumerator() => new(this); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public struct Enumerator : IEnumerator - { + /// + /// Represents an enumerator for the CircularBuffer. + /// + public struct Enumerator : IEnumerator { private readonly CircularBuffer _buffer; private int _index; private double _current; [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal Enumerator(CircularBuffer buffer) - { + internal Enumerator(CircularBuffer buffer) { _buffer = buffer; _index = -1; _current = default; } + /// + /// Advances the enumerator to the next element of the buffer. + /// + /// true if the enumerator was successfully advanced to the next element; false if the enumerator has passed the end of the collection. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool MoveNext() - { + public bool MoveNext() { if (_index + 1 >= _buffer._size) return false; @@ -116,92 +145,122 @@ public class CircularBuffer : IEnumerable return true; } + /// + /// Gets the element in the buffer at the current position of the enumerator. + /// public double Current => _current; object IEnumerator.Current => Current; - public void Reset() - { + /// + /// Sets the enumerator to its initial position, which is before the first element in the buffer. + /// + public void Reset() { _index = -1; _current = default; } + /// + /// Disposes the enumerator. + /// public void Dispose() { } } + /// + /// Copies the elements of the buffer to an array, starting at a particular array index. + /// + /// The one-dimensional array that is the destination of the elements copied from the buffer. + /// The zero-based index in array at which copying begins. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void CopyTo(double[] destination, int destinationIndex) - { + public void CopyTo(double[] destination, int destinationIndex) { if (_size == 0) return; - if (_start + _size <= Capacity) - { + if (_start + _size <= Capacity) { Array.Copy(_buffer, _start, destination, destinationIndex, _size); - } - else - { + } else { int firstPartLength = Capacity - _start; Array.Copy(_buffer, _start, destination, destinationIndex, firstPartLength); Array.Copy(_buffer, 0, destination, destinationIndex + firstPartLength, _size - firstPartLength); } } + /// + /// Returns a read-only span over the contents of the buffer. + /// + /// A read-only span over the buffer contents. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ReadOnlySpan GetSpan() - { + public ReadOnlySpan GetSpan() { if (_size == 0) return ReadOnlySpan.Empty; - if (_start + _size <= Capacity) - { + if (_start + _size <= Capacity) { return new ReadOnlySpan(_buffer, _start, _size); - } - else - { + } else { return new ReadOnlySpan(ToArray()); } } + /// + /// Gets the internal buffer array. + /// public double[] InternalBuffer => _buffer; + /// + /// Returns a read-only span over the entire internal buffer. + /// + /// A read-only span over the entire internal buffer. [MethodImpl(MethodImplOptions.AggressiveInlining)] public ReadOnlySpan GetInternalSpan() => _buffer.AsSpan(); + /// + /// Removes all elements from the buffer. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Clear() - { + public void Clear() { Array.Clear(_buffer, 0, _buffer.Length); _start = 0; _size = 0; } + /// + /// Returns the maximum value in the buffer. + /// + /// The maximum value in the buffer. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public double Max() - { + public double Max() { if (_size == 0) ThrowInvalidOperationException(); return MaxSimd(); } + /// + /// Returns the minimum value in the buffer. + /// + /// The minimum value in the buffer. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public double Min() - { + public double Min() { if (_size == 0) ThrowInvalidOperationException(); return MinSimd(); } + /// + /// Computes the sum of all values in the buffer. + /// + /// The sum of all values in the buffer. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public double Sum() - { + public double Sum() { return SumSimd(); } + /// + /// Computes the average of all values in the buffer. + /// + /// The average of all values in the buffer. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public double Average() - { + public double Average() { if (_size == 0) ThrowInvalidOperationException(); @@ -209,26 +268,22 @@ public class CircularBuffer : IEnumerable } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private double MaxSimd() - { + private double MaxSimd() { var span = GetSpan(); var vectorSize = Vector.Count; var maxVector = new Vector(double.MinValue); int i = 0; - for (; i <= span.Length - vectorSize; i += vectorSize) - { + for (; i <= span.Length - vectorSize; i += vectorSize) { maxVector = Vector.Max(maxVector, new Vector(span.Slice(i, vectorSize))); } double max = double.MinValue; - for (int j = 0; j < vectorSize; j++) - { + for (int j = 0; j < vectorSize; j++) { max = Math.Max(max, maxVector[j]); } - for (; i < span.Length; i++) - { + for (; i < span.Length; i++) { max = Math.Max(max, span[i]); } @@ -236,26 +291,22 @@ public class CircularBuffer : IEnumerable } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private double MinSimd() - { + private double MinSimd() { var span = GetSpan(); var vectorSize = Vector.Count; var minVector = new Vector(double.MaxValue); int i = 0; - for (; i <= span.Length - vectorSize; i += vectorSize) - { + for (; i <= span.Length - vectorSize; i += vectorSize) { minVector = Vector.Min(minVector, new Vector(span.Slice(i, vectorSize))); } double min = double.MaxValue; - for (int j = 0; j < vectorSize; j++) - { + for (int j = 0; j < vectorSize; j++) { min = Math.Min(min, minVector[j]); } - for (; i < span.Length; i++) - { + for (; i < span.Length; i++) { min = Math.Min(min, span[i]); } @@ -263,45 +314,46 @@ public class CircularBuffer : IEnumerable } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private double SumSimd() - { + private double SumSimd() { var span = GetSpan(); var vectorSize = Vector.Count; var sumVector = Vector.Zero; int i = 0; - for (; i <= span.Length - vectorSize; i += vectorSize) - { + for (; i <= span.Length - vectorSize; i += vectorSize) { sumVector += new Vector(span.Slice(i, vectorSize)); } double sum = 0; - for (int j = 0; j < vectorSize; j++) - { + for (int j = 0; j < vectorSize; j++) { sum += sumVector[j]; } - for (; i < span.Length; i++) - { + for (; i < span.Length; i++) { sum += span[i]; } return sum; } - public double[] ToArray() - { + /// + /// Copies the buffer elements to a new array. + /// + /// An array containing copies of the buffer elements. + public double[] ToArray() { double[] array = new double[_size]; CopyTo(array, 0); return array; } - public void ParallelOperation(Func operation) - { + /// + /// Performs a parallel operation on the buffer elements. + /// + /// The operation to perform on each partition of the buffer. + public void ParallelOperation(Func operation) { const int MinimumPartitionSize = 1024; - if (_size < MinimumPartitionSize) - { + if (_size < MinimumPartitionSize) { var span = GetSpan(); var array = span.ToArray(); operation(array, 0, array.Length); @@ -311,8 +363,7 @@ public class CircularBuffer : IEnumerable int partitionCount = Environment.ProcessorCount; int partitionSize = _size / partitionCount; - if (partitionSize < MinimumPartitionSize) - { + if (partitionSize < MinimumPartitionSize) { partitionCount = Math.Max(1, _size / MinimumPartitionSize); partitionSize = _size / partitionCount; } @@ -320,13 +371,10 @@ public class CircularBuffer : IEnumerable var buffer = ToArray(); var results = new double[partitionCount]; - Parallel.For(0, partitionCount, i => - { + Parallel.For(0, partitionCount, i => { int start = i * partitionSize; int length = (i == partitionCount - 1) ? _size - start : partitionSize; results[i] = operation(buffer, start, length); }); - } - } \ No newline at end of file diff --git a/lib/statistics/Curvature.cs b/lib/statistics/Curvature.cs index 4f3f6df7..838ffeab 100644 --- a/lib/statistics/Curvature.cs +++ b/lib/statistics/Curvature.cs @@ -1,128 +1,156 @@ -using System; -using System.Collections.Generic; +namespace QuanTAlib; -namespace QuanTAlib +/// +/// Calculates the rate of change of the slope over a specified period. +/// Provides insights into trend acceleration or deceleration. +/// +public class Curvature : AbstractBase { - public class Curvature : AbstractBase + private readonly int _period; + private readonly Slope _slopeCalculator; + private readonly CircularBuffer _slopeBuffer; + + public double? Intercept { get; private set; } + public double? StdDev { get; private set; } + public double? RSquared { get; private set; } + 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. + /// + public Curvature(int period) { - private readonly int _period; - private readonly Slope _slopeCalculator; - private readonly CircularBuffer _slopeBuffer; - - public double? Intercept { get; private set; } - public double? StdDev { get; private set; } - public double? RSquared { get; private set; } - public double? Line { get; private set; } - - public Curvature(int period) + if (period <= 2) { - if (period <= 2) + throw new ArgumentOutOfRangeException(nameof(period), period, + "Period must be greater than 2 for Curvature calculation."); + } + _period = period; + WarmupPeriod = period * 2 - 1; // Number of points needed for period number of slopes + _slopeCalculator = new Slope(period); + _slopeBuffer = new CircularBuffer(period); + Name = $"Curvature(period={period})"; + + 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. + 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(); + _slopeBuffer.Clear(); + Intercept = null; + StdDev = null; + RSquared = null; + Line = null; + } + + /// + /// Manages the state of the indicator. + /// + /// Indicates if the current data point is new. + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Input.Value; + _index++; + } + } + + /// + /// 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); + + var slopeResult = _slopeCalculator.Calc(Input); + _slopeBuffer.Add(slopeResult.Value, Input.IsNew); + + double curvature = 0; + + if (_slopeBuffer.Count < 2) + { + return curvature; // Not enough points for calculation + } + + int count = Math.Min(_slopeBuffer.Count, _period); + var slopes = _slopeBuffer.GetSpan().ToArray(); + + // Calculate averages + double sumX = 0, sumY = 0; + for (int i = 0; i < count; i++) + { + sumX += i + 1; + sumY += slopes[i]; + } + 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; + } + + if (sumSqX > 0) + { + curvature = sumSqXY / sumSqX; + Intercept = avgY - (curvature * avgX); + + // Calculate Standard Deviation and R-Squared + double stdDevX = Math.Sqrt(sumSqX / count); + double stdDevY = Math.Sqrt(sumSqY / count); + StdDev = stdDevY; + + if (stdDevX * stdDevY != 0) { - throw new ArgumentOutOfRangeException(nameof(period), period, - "Period must be greater than 2 for Curvature calculation."); + double r = sumSqXY / (stdDevX * stdDevY) / count; + RSquared = r * r; } - _period = period; - WarmupPeriod = period * 2 - 1; // We need this many points to get period number of slopes - _slopeCalculator = new Slope(period); - _slopeBuffer = new CircularBuffer(period); - Name = $"Curvature(period={period})"; - Init(); + // Calculate last Line value (y = mx + b) + Line = (curvature * count) + Intercept; } - - public Curvature(object source, int period) : this(period) + else { - var pubEvent = source.GetType().GetEvent("Pub"); - pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); - } - - public override void Init() - { - base.Init(); - _slopeBuffer.Clear(); Intercept = null; StdDev = null; RSquared = null; Line = null; } - protected override void ManageState(bool isNew) - { - if (isNew) - { - _lastValidValue = Input.Value; - _index++; - } - } - - protected override double Calculation() - { - ManageState(Input.IsNew); - - // Calculate slope - var slopeResult = _slopeCalculator.Calc(Input); - _slopeBuffer.Add(slopeResult.Value, Input.IsNew); - - double curvature = 0; - - if (_slopeBuffer.Count < 2) - { - return curvature; // Return 0 when there are fewer than 2 slope points - } - - int count = Math.Min(_slopeBuffer.Count, _period); - var slopes = _slopeBuffer.GetSpan().ToArray(); - - // Calculate averages - double sumX = 0, sumY = 0; - for (int i = 0; i < count; i++) - { - sumX += i + 1; - sumY += slopes[i]; - } - 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; - } - - if (sumSqX > 0) - { - curvature = sumSqXY / sumSqX; - Intercept = avgY - (curvature * avgX); - - // Calculate Standard Deviation and R-Squared - double stdDevX = Math.Sqrt(sumSqX / count); - double stdDevY = Math.Sqrt(sumSqY / count); - StdDev = stdDevY; - - if (stdDevX * stdDevY != 0) - { - double r = sumSqXY / (stdDevX * stdDevY) / count; - RSquared = r * r; - } - - // Calculate last Line value (y = mx + b) - Line = (curvature * count) + Intercept; - } - else - { - Intercept = null; - StdDev = null; - RSquared = null; - Line = null; - } - - IsHot = _slopeBuffer.Count == _period; - return curvature; - } + IsHot = _slopeBuffer.Count == _period; + return curvature; } } \ No newline at end of file diff --git a/lib/statistics/Entropy.cs b/lib/statistics/Entropy.cs index a5f561bb..df59cb93 100644 --- a/lib/statistics/Entropy.cs +++ b/lib/statistics/Entropy.cs @@ -1,19 +1,27 @@ namespace QuanTAlib; -using System; -using System.Linq; - -// Shannon's Entropy calculation +/// +/// Measures the unpredictability of data using Shannon's Entropy. +/// Provides insights into the randomness or information content of the time series. +/// public class Entropy : AbstractBase { 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. + /// public Entropy(int period) : base() { if (period < 2) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2 for entropy calculation."); + throw new ArgumentOutOfRangeException(nameof(period), + "Period must be greater than or equal to 2 for entropy calculation."); } Period = period; WarmupPeriod = 2; @@ -22,18 +30,30 @@ 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. 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) @@ -43,6 +63,17 @@ 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); @@ -70,9 +101,11 @@ public class Entropy : AbstractBase double maxEntropy = Math.Log2(uniqueValueCount); entropy = entropy == 0 ? 1 : entropy / maxEntropy; - } - else { entropy = 1; } + else + { + entropy = 1; // Default to maximum entropy when insufficient data + } IsHot = _buffer.Count >= Period; return entropy; diff --git a/lib/statistics/Kurtosis.cs b/lib/statistics/Kurtosis.cs index 895a175a..af13ac0b 100644 --- a/lib/statistics/Kurtosis.cs +++ b/lib/statistics/Kurtosis.cs @@ -1,16 +1,27 @@ namespace QuanTAlib; -// Excess kurtosis calculated with Sheskin Algorithm +/// +/// Calculates excess kurtosis using the Sheskin Algorithm. +/// Measures the "tailedness" of the probability distribution of a real-valued random variable. +/// public class Kurtosis : AbstractBase { 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. + /// public Kurtosis(int period) : base() { if (period < 4) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 4 for kurtosis calculation."); + throw new ArgumentOutOfRangeException(nameof(period), + "Period must be greater than or equal to 4 for kurtosis calculation."); } Period = period; WarmupPeriod = Period - 1; @@ -19,18 +30,30 @@ 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. 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) @@ -40,6 +63,17 @@ 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. + /// protected override double Calculation() { ManageState(Input.IsNew); @@ -65,7 +99,7 @@ public class Kurtosis : AbstractBase double variance = s2 / (n - 1); - // Using the Sheskin Algorithm for kurtosis + // Sheskin Algorithm 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 4eb63fcf..b96fa31c 100644 --- a/lib/statistics/Max.cs +++ b/lib/statistics/Max.cs @@ -1,80 +1,114 @@ -using System; +namespace QuanTAlib; -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. +/// +public class Max : AbstractBase { - public class Max : AbstractBase + private readonly int Period; + private readonly CircularBuffer _buffer; + private readonly double _halfLife; + private double _currentMax, _p_currentMax; + private int _timeSinceNewMax, _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. Default is 0. + /// + /// Thrown when the period is less than 1 or decay is negative. + /// + public Max(int period, double decay = 0) : base() { - private readonly int Period; - private readonly CircularBuffer _buffer; - private readonly double _halfLife; - private double _currentMax, _p_currentMax; - private int _timeSinceNewMax, _p_timeSinceNewMax; - - public Max(int period, double decay = 0) : base() + if (period < 1) { - if (period < 1) - { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); - } - if (decay < 0) - { - throw new ArgumentOutOfRangeException(nameof(decay), "Half-life must be non-negative."); - } - Period = period; - WarmupPeriod = 0; - _buffer = new CircularBuffer(period); - _halfLife = decay * 0.1; - Name = $"Max(period={period}, halfLife={decay:F2})"; - Init(); + throw new ArgumentOutOfRangeException(nameof(period), + "Period must be greater than or equal to 1."); } - - public Max(object source, int period, double decay = 0) : this(period, decay) + if (decay < 0) { - var pubEvent = source.GetType().GetEvent("Pub"); - pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + throw new ArgumentOutOfRangeException(nameof(decay), + "Half-life must be non-negative."); } + Period = period; + WarmupPeriod = 0; + _buffer = new CircularBuffer(period); + _halfLife = decay * 0.1; + Name = $"Max(period={period}, halfLife={decay:F2})"; + Init(); + } - public override void 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. + 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(); + _currentMax = double.MinValue; + _timeSinceNewMax = 0; + } + + /// + /// Manages the state of the indicator. + /// + /// Indicates if the current data point is new. + protected override void ManageState(bool isNew) + { + if (isNew) { - base.Init(); - _currentMax = double.MinValue; + _p_currentMax = _currentMax; + _lastValidValue = Input.Value; + _index++; + _timeSinceNewMax++; + _p_timeSinceNewMax = _timeSinceNewMax; + } + else + { + _currentMax = _p_currentMax; + _timeSinceNewMax = _p_timeSinceNewMax; + } + } + + /// + /// 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); + + if (Input.Value >= _currentMax) + { + _currentMax = Input.Value; _timeSinceNewMax = 0; } - protected override void ManageState(bool isNew) - { - if (isNew) - { - _p_currentMax = _currentMax; - _lastValidValue = Input.Value; - _index++; - _timeSinceNewMax++; - _p_timeSinceNewMax = _timeSinceNewMax; - } - else - { - _currentMax = _p_currentMax; - _timeSinceNewMax = _p_timeSinceNewMax; - } - } + double decayRate = 1 - Math.Exp(-_halfLife * _timeSinceNewMax / Period); + _currentMax = _currentMax - decayRate * (_currentMax - _buffer.Average()); + _currentMax = Math.Min(_currentMax, _buffer.Max()); - protected override double Calculation() - { - ManageState(Input.IsNew); - _buffer.Add(Input.Value, Input.IsNew); - - if (Input.Value >= _currentMax) - { - _currentMax = Input.Value; - _timeSinceNewMax = 0; - } - - double decayRate = 1 - Math.Exp(-_halfLife * _timeSinceNewMax / Period); - _currentMax = _currentMax - decayRate * (_currentMax - _buffer.Average()); - _currentMax = Math.Min(_currentMax, _buffer.Max()); - - IsHot = true; - return _currentMax; - } + IsHot = true; + return _currentMax; } -} \ No newline at end of file +} diff --git a/lib/statistics/Median.cs b/lib/statistics/Median.cs index a1a95783..666dc6a8 100644 --- a/lib/statistics/Median.cs +++ b/lib/statistics/Median.cs @@ -1,69 +1,99 @@ -using System; -using System.Linq; +namespace QuanTAlib; -namespace QuanTAlib +/// +/// Calculates the median value over a specified period. +/// Provides a measure of central tendency that is robust to outliers. +/// +public class Median : AbstractBase { - public class Median : AbstractBase + 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. + /// + public Median(int period) : base() { - private readonly int Period; - private readonly CircularBuffer _buffer; - - public Median(int period) : base() + if (period < 1) { - if (period < 1) - { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); - } - Period = period; - WarmupPeriod = period; - _buffer = new CircularBuffer(period); - Name = $"Median(period={period})"; - Init(); + throw new ArgumentOutOfRangeException(nameof(period), + "Period must be greater than or equal to 1."); } + Period = period; + WarmupPeriod = period; + _buffer = new CircularBuffer(period); + Name = $"Median(period={period})"; + Init(); + } - public Median(object source, int period) : this(period) + /// + /// 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. + public Median(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + /// + /// Manages the state of the indicator. + /// + /// Indicates if the current data point is new. + protected override void ManageState(bool isNew) + { + if (isNew) { - var pubEvent = source.GetType().GetEvent("Pub"); - pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + _lastValidValue = Input.Value; + _index++; } + } - protected override void ManageState(bool isNew) + /// + /// 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); + _buffer.Add(Input.Value, Input.IsNew); + + double median; + if (_index >= Period) { - if (isNew) + var sortedValues = _buffer.GetSpan().ToArray(); + Array.Sort(sortedValues); + int middleIndex = sortedValues.Length / 2; + + if (sortedValues.Length % 2 == 0) { - _lastValidValue = Input.Value; - _index++; - } - } - - protected override double Calculation() - { - ManageState(Input.IsNew); - _buffer.Add(Input.Value, Input.IsNew); - - double median; - if (_index >= Period) - { - var sortedValues = _buffer.GetSpan().ToArray(); - Array.Sort(sortedValues); - int middleIndex = sortedValues.Length / 2; - - if (sortedValues.Length % 2 == 0) - { - median = (sortedValues[middleIndex - 1] + sortedValues[middleIndex]) / 2.0; - } - else - { - median = sortedValues[middleIndex]; - } + // Even number of values: average of two middle values + median = (sortedValues[middleIndex - 1] + sortedValues[middleIndex]) / 2.0; } else { - median = _buffer.Average(); // Use average until we have enough data points + // Odd number of values: middle value + median = sortedValues[middleIndex]; } - - IsHot = _index >= WarmupPeriod; - return median; } + else + { + // Not enough data, use average as temporary measure + median = _buffer.Average(); + } + + IsHot = _index >= WarmupPeriod; + return median; } -} \ No newline at end of file +} diff --git a/lib/statistics/Min.cs b/lib/statistics/Min.cs index a6b30278..a525d966 100644 --- a/lib/statistics/Min.cs +++ b/lib/statistics/Min.cs @@ -1,80 +1,106 @@ -using System; +namespace QuanTAlib; -namespace QuanTAlib -{ - public class Min : AbstractBase - { - private readonly int Period; - private readonly CircularBuffer _buffer; - private readonly double _halfLife; - private double _currentMin, _p_currentMin; - private int _timeSinceNewMin, _p_timeSinceNewMin; +/// +/// 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. +/// +/// +/// 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. +/// +public class Min : AbstractBase { + private readonly int Period; + private readonly CircularBuffer _buffer; + private readonly double _halfLife; + private double _currentMin, _p_currentMin; + private int _timeSinceNewMin, _p_timeSinceNewMin; - public Min(int period, double decay = 0) : base() - { - if (period < 1) - { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); - } - if (decay < 0) - { - throw new ArgumentOutOfRangeException(nameof(decay), "Half-life must be non-negative."); - } - Period = period; - WarmupPeriod = 0; - _buffer = new CircularBuffer(period); - _halfLife = decay * 0.1; - Name = $"Min(period={period}, halfLife={decay:F2})"; - Init(); + /// + /// 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 (default is 0). + /// + /// Thrown when period is less than 1 or decay is negative. + /// + public Min(int period, double decay = 0) : base() { + if (period < 1) { + throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); } - - public Min(object source, int period, double decay = 0) : this(period, decay) - { - var pubEvent = source.GetType().GetEvent("Pub"); - pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + if (decay < 0) { + throw new ArgumentOutOfRangeException(nameof(decay), "Half-life must be non-negative."); } + Period = period; + WarmupPeriod = 0; + _buffer = new CircularBuffer(period); + _halfLife = decay * 0.1; + Name = $"Min(period={period}, halfLife={decay:F2})"; + Init(); + } - public override void Init() - { - base.Init(); - _currentMin = double.MaxValue; + /// + /// 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 (default is 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(); + _currentMin = double.MaxValue; + _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) { + _p_currentMin = _currentMin; + _lastValidValue = Input.Value; + _index++; + _timeSinceNewMin++; + _p_timeSinceNewMin = _timeSinceNewMin; + } else { + _currentMin = _p_currentMin; + _timeSinceNewMin = _p_timeSinceNewMin; + } + } + + /// + /// 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); + + if (Input.Value <= _currentMin) { + _currentMin = Input.Value; _timeSinceNewMin = 0; } - protected override void ManageState(bool isNew) - { - if (isNew) - { - _p_currentMin = _currentMin; - _lastValidValue = Input.Value; - _index++; - _timeSinceNewMin++; - _p_timeSinceNewMin = _timeSinceNewMin; - } - else - { - _currentMin = _p_currentMin; - _timeSinceNewMin = _p_timeSinceNewMin; - } - } + double decayRate = 1 - Math.Exp(-_halfLife * _timeSinceNewMin / Period); + _currentMin = _currentMin + decayRate * (_buffer.Average() - _currentMin); + _currentMin = Math.Max(_currentMin, _buffer.Min()); - protected override double Calculation() - { - ManageState(Input.IsNew); - _buffer.Add(Input.Value, Input.IsNew); - - if (Input.Value <= _currentMin) - { - _currentMin = Input.Value; - _timeSinceNewMin = 0; - } - - double decayRate = 1 - Math.Exp(-_halfLife * _timeSinceNewMin / Period); - _currentMin = _currentMin + decayRate * (_buffer.Average() - _currentMin); - _currentMin = Math.Max(_currentMin, _buffer.Min()); - - IsHot = true; - return _currentMin; - } + IsHot = true; + return _currentMin; } -} \ No newline at end of file +} diff --git a/lib/statistics/Mode.cs b/lib/statistics/Mode.cs index c5f23e52..bcd3ab57 100644 --- a/lib/statistics/Mode.cs +++ b/lib/statistics/Mode.cs @@ -1,14 +1,27 @@ namespace QuanTAlib; -public class Mode : AbstractBase -{ +/// +/// 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. +/// +/// +/// 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. +/// +public class Mode : AbstractBase { private readonly int Period; private readonly CircularBuffer _buffer; - public Mode(int period) : base() - { - if (period < 1) - { + /// + /// 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. + /// + public Mode(int period) : base() { + if (period < 1) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); } Period = period; @@ -18,29 +31,45 @@ public class Mode : AbstractBase Init(); } - public Mode(object source, int period) : this(period) - { + /// + /// 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. + public Mode(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) - { + /// + /// 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) { _lastValidValue = Input.Value; _index++; } } - protected override double Calculation() - { + /// + /// 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); _buffer.Add(Input.Value, Input.IsNew); double mode; - if (_index >= Period) - { + if (_index >= Period) { var values = _buffer.GetSpan().ToArray(); var groupedValues = values.GroupBy(v => v) .OrderByDescending(g => g.Count()) @@ -53,9 +82,7 @@ public class Mode : AbstractBase .ToList(); mode = modes.Average(); // If there are multiple modes, we return their average - } - else - { + } else { mode = _buffer.Average(); // Use average until we have enough data points } diff --git a/lib/statistics/Percentile.cs b/lib/statistics/Percentile.cs index 5377e840..a985a637 100644 --- a/lib/statistics/Percentile.cs +++ b/lib/statistics/Percentile.cs @@ -1,22 +1,33 @@ namespace QuanTAlib; -using System; -using System.Linq; - -public class Percentile : AbstractBase -{ +/// +/// Represents a percentile calculator that determines the value at a specified percentile +/// in a given period of data points. +/// +/// +/// 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. +/// +public class Percentile : AbstractBase { private readonly int Period; private readonly double Percent; private readonly CircularBuffer _buffer; - public Percentile(int period, double percent) : base() - { - if (period < 2) - { + /// + /// 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). + /// + /// Thrown when period is less than 2 or percent is not between 0 and 100. + /// + public Percentile(int period, double percent) : base() { + if (period < 2) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2 for percentile calculation."); } - if (percent < 0 || percent > 100) - { + if (percent < 0 || percent > 100) { throw new ArgumentOutOfRangeException(nameof(percent), "Percent must be between 0 and 100."); } Period = period; @@ -27,35 +38,54 @@ public class Percentile : AbstractBase Init(); } - public Percentile(object source, int period, double percent) : this(period, percent) - { + /// + /// 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). + public Percentile(object source, int period, double percent) : this(period, percent) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - public override void Init() - { + /// + /// Initializes the Percentile instance by clearing the buffer. + /// + public override void Init() { base.Init(); _buffer.Clear(); } - protected override void ManageState(bool isNew) - { - if (isNew) - { + /// + /// 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) { _lastValidValue = Input.Value; _index++; } } -protected override double Calculation() - { + /// + /// 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); _buffer.Add(Input.Value, Input.IsNew); double result; - if (_buffer.Count >= Period) - { + if (_buffer.Count >= Period) { var values = _buffer.GetSpan().ToArray(); Array.Sort(values); @@ -63,21 +93,16 @@ protected override double Calculation() int lowerIndex = (int)Math.Floor(position); int upperIndex = (int)Math.Ceiling(position); - if (lowerIndex == upperIndex) - { + if (lowerIndex == upperIndex) { result = values[lowerIndex]; - } - else - { + } else { // Interpolate between the two nearest values double lowerValue = values[lowerIndex]; double upperValue = values[upperIndex]; double fraction = position - lowerIndex; result = lowerValue + (upperValue - lowerValue) * fraction; } - } - else - { + } else { // Use average for insufficient data, like the Median class result = _buffer.Average(); } diff --git a/lib/statistics/Skew.cs b/lib/statistics/Skew.cs index f70cec0a..0d32f72a 100644 --- a/lib/statistics/Skew.cs +++ b/lib/statistics/Skew.cs @@ -1,17 +1,28 @@ namespace QuanTAlib; -using System; -using System.Linq; - -public class Skew : AbstractBase -{ +/// +/// Represents a skewness calculator that measures the asymmetry of the probability +/// distribution of a real-valued random variable about its mean. +/// +/// +/// 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. +/// +public class Skew : AbstractBase { private readonly int Period; private readonly CircularBuffer _buffer; - public Skew(int period) : base() - { - if (period < 3) - { + /// + /// 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. + /// + public Skew(int period) : base() { + if (period < 3) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 3 for skewness calculation."); } Period = period; @@ -21,36 +32,54 @@ public class Skew : AbstractBase Init(); } - public Skew(object source, int period) : this(period) - { + /// + /// 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. + public Skew(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - public override void Init() - { + /// + /// Initializes the Skew instance by clearing the buffer. + /// + public override void Init() { base.Init(); _buffer.Clear(); } - protected override void ManageState(bool isNew) - { - if (isNew) - { + /// + /// 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) { _lastValidValue = Input.Value; _index++; } } - protected override double Calculation() - { + /// + /// 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. + /// + 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) { // We need at least 3 data points for skewness var values = _buffer.GetSpan().ToArray(); double mean = values.Average(); double n = values.Length; @@ -58,8 +87,7 @@ public class Skew : AbstractBase double sumCubedDeviations = 0; double sumSquaredDeviations = 0; - foreach (var value in values) - { + foreach (var value in values) { double deviation = value - mean; sumCubedDeviations += Math.Pow(deviation, 3); sumSquaredDeviations += Math.Pow(deviation, 2); @@ -70,8 +98,7 @@ public class Skew : AbstractBase 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 ec2d97ea..293144ac 100644 --- a/lib/statistics/Slope.cs +++ b/lib/statistics/Slope.cs @@ -1,128 +1,147 @@ -using System; -using System.Collections.Generic; +namespace QuanTAlib; -namespace QuanTAlib -{ - public class Slope : AbstractBase - { - private readonly int _period; - private readonly CircularBuffer _buffer; - private readonly CircularBuffer _timeBuffer; +/// +/// Represents a slope calculator that performs linear regression on a series of data points. +/// +/// +/// 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. +/// +public class Slope : AbstractBase { + private readonly int _period; + private readonly CircularBuffer _buffer; + private readonly CircularBuffer _timeBuffer; + public double? Intercept { get; private set; } + public double? StdDev { get; private set; } + public double? RSquared { get; private set; } + public double? Line { get; private set; } - public double? Intercept { get; private set; } - public double? StdDev { get; private set; } - public double? RSquared { get; private set; } - 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. + /// + public Slope(int period) { + if (period <= 1) { + throw new ArgumentOutOfRangeException(nameof(period), period, + "Period must be greater than 1 for Slope/Linear Regression."); + } + _period = period; + WarmupPeriod = period; + _buffer = new CircularBuffer(period); + _timeBuffer = new CircularBuffer(period); + Name = $"Slope(period={period})"; - public Slope(int period) - { - if (period <= 1) - { - throw new ArgumentOutOfRangeException(nameof(period), period, - "Period must be greater than 1 for Slope/Linear Regression."); + 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. + 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(); + _buffer.Clear(); + _timeBuffer.Clear(); + Intercept = null; + StdDev = null; + RSquared = null; + 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) { + _lastValidValue = Input.Value; + _index++; + } + } + + /// + /// 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. + /// + 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 < 2) { + return slope; // Return 0 when there are fewer than 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; + + // Least squares method + double sumSqX = 0, sumSqY = 0, sumSqXY = 0; + for (int i = 0; i < count; i++) { + double devX = (i + 1) - avgX; + double devY = values[i] - avgY; + sumSqX += devX * devX; + sumSqY += devY * devY; + sumSqXY += devX * devY; + } + + if (sumSqX > 0) { + slope = sumSqXY / sumSqX; + Intercept = avgY - (slope * avgX); + + // Calculate Standard Deviation and R-Squared + double stdDevX = Math.Sqrt(sumSqX / count); + double stdDevY = Math.Sqrt(sumSqY / count); + StdDev = stdDevY; + + if (stdDevX * stdDevY != 0) { + double r = sumSqXY / (stdDevX * stdDevY) / count; + RSquared = r * r; } - _period = period; - WarmupPeriod = period; - _buffer = new CircularBuffer(period); - _timeBuffer = new CircularBuffer(period); - Name = $"Slope(period={period})"; - Init(); - } - - public Slope(object source, int period) : this(period) - { - var pubEvent = source.GetType().GetEvent("Pub"); - pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); - } - - public override void Init() - { - base.Init(); - _buffer.Clear(); - _timeBuffer.Clear(); + // Calculate last Line value (y = mx + b) + Line = (slope * count) + Intercept; + } else { Intercept = null; StdDev = null; RSquared = null; Line = null; } - protected override void ManageState(bool isNew) - { - if (isNew) - { - _lastValidValue = Input.Value; - _index++; - } - } - - 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 < 2) - { - return slope; // Return 0 when there are fewer than 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; - - // Least squares method - double sumSqX = 0, sumSqY = 0, sumSqXY = 0; - for (int i = 0; i < count; i++) - { - double devX = (i + 1) - avgX; - double devY = values[i] - avgY; - sumSqX += devX * devX; - sumSqY += devY * devY; - sumSqXY += devX * devY; - } - - if (sumSqX > 0) - { - slope = sumSqXY / sumSqX; - Intercept = avgY - (slope * avgX); - - // Calculate Standard Deviation and R-Squared - double stdDevX = Math.Sqrt(sumSqX / count); - double stdDevY = Math.Sqrt(sumSqY / count); - StdDev = stdDevY; - - if (stdDevX * stdDevY != 0) - { - double r = sumSqXY / (stdDevX * stdDevY) / count; - RSquared = r * r; - } - - // Calculate last Line value (y = mx + b) - Line = (slope * count) + Intercept; - } - else - { - Intercept = null; - StdDev = null; - RSquared = null; - Line = null; - } - - IsHot = _buffer.Count == _period; - return slope; - } + IsHot = _buffer.Count == _period; + return slope; } -} \ No newline at end of file +} diff --git a/lib/statistics/Stddev.cs b/lib/statistics/Stddev.cs index ca49b2e5..8d5c30e9 100644 --- a/lib/statistics/Stddev.cs +++ b/lib/statistics/Stddev.cs @@ -1,69 +1,105 @@ -using System; -using System.Linq; +namespace QuanTAlib; -namespace QuanTAlib -{ - public class Stddev : AbstractBase - { - private readonly int Period; - private readonly bool IsPopulation; - private readonly CircularBuffer _buffer; +/// +/// Represents a standard deviation calculator that measures the amount of variation or +/// dispersion of a set of values. +/// +/// +/// 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. +/// +public class Stddev : AbstractBase { + private readonly int Period; + private readonly bool IsPopulation; + private readonly CircularBuffer _buffer; - public Stddev(int period, bool isPopulation = false) : base() - { - if (period < 2) - { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2."); - } - Period = period; - IsPopulation = isPopulation; - WarmupPeriod = 0; - _buffer = new CircularBuffer(period); - Name = $"Stddev(period={period}, population={isPopulation})"; - Init(); + /// + /// 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. + /// + public Stddev(int period, bool isPopulation = false) : base() { + if (period < 2) { + throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2."); } + Period = period; + IsPopulation = isPopulation; + WarmupPeriod = 0; + _buffer = new CircularBuffer(period); + Name = $"Stddev(period={period}, population={isPopulation})"; + Init(); + } - 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 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. + /// + public Stddev(object source, int period, bool isPopulation = false) : this(period, isPopulation) { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } - public override void Init() - { - base.Init(); - _buffer.Clear(); - } + /// + /// Initializes the Stddev instance by clearing the buffer. + /// + public override void Init() { + base.Init(); + _buffer.Clear(); + } - protected override void ManageState(bool isNew) - { - if (isNew) - { - _lastValidValue = Input.Value; - _index++; - } - } - - protected override double Calculation() - { - ManageState(Input.IsNew); - - _buffer.Add(Input.Value, Input.IsNew); - - double stddev = 0; - if (_buffer.Count > 1) - { - var values = _buffer.GetSpan().ToArray(); - double mean = values.Average(); - double sumOfSquaredDifferences = values.Sum(x => Math.Pow(x - mean, 2)); - - double divisor = IsPopulation ? _buffer.Count : _buffer.Count - 1; - double variance = sumOfSquaredDifferences / divisor; - stddev = Math.Sqrt(variance); - } - - IsHot = true; // StdDev calc is valid from bar 1 - return stddev; + /// + /// 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) { + _lastValidValue = Input.Value; + _index++; } } -} \ No newline at end of file + + /// + /// 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. + /// + protected override double Calculation() { + ManageState(Input.IsNew); + + _buffer.Add(Input.Value, Input.IsNew); + + double stddev = 0; + if (_buffer.Count > 1) { + var values = _buffer.GetSpan().ToArray(); + double mean = values.Average(); + double sumOfSquaredDifferences = values.Sum(x => Math.Pow(x - mean, 2)); + + double divisor = IsPopulation ? _buffer.Count : _buffer.Count - 1; + double variance = sumOfSquaredDifferences / divisor; + stddev = Math.Sqrt(variance); + } + + IsHot = true; // StdDev calc is valid from bar 1 + return stddev; + } +} diff --git a/lib/statistics/Variance.cs b/lib/statistics/Variance.cs index 4dc37329..dc910203 100644 --- a/lib/statistics/Variance.cs +++ b/lib/statistics/Variance.cs @@ -1,68 +1,104 @@ -using System; -using System.Linq; +namespace QuanTAlib; -namespace QuanTAlib -{ - public class Variance : AbstractBase - { - private readonly int Period; - private readonly bool IsPopulation; - private readonly CircularBuffer _buffer; +/// +/// Represents a variance calculator that measures the spread of a set of numbers +/// from their average value. +/// +/// +/// 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. +/// +public class Variance : AbstractBase { + private readonly int Period; + private readonly bool IsPopulation; + private readonly CircularBuffer _buffer; - public Variance(int period, bool isPopulation = false) : base() - { - if (period < 2) - { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2."); - } - Period = period; - IsPopulation = isPopulation; - WarmupPeriod = 0; - _buffer = new CircularBuffer(period); - Name = $"Variance(period={period}, population={isPopulation})"; - Init(); + /// + /// 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. + /// + public Variance(int period, bool isPopulation = false) : base() { + if (period < 2) { + throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2."); } + Period = period; + IsPopulation = isPopulation; + WarmupPeriod = 0; + _buffer = new CircularBuffer(period); + Name = $"Variance(period={period}, population={isPopulation})"; + Init(); + } - 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 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. + /// + public Variance(object source, int period, bool isPopulation = false) : this(period, isPopulation) { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } - public override void Init() - { - base.Init(); - _buffer.Clear(); - } + /// + /// Initializes the Variance instance by clearing the buffer. + /// + public override void Init() { + base.Init(); + _buffer.Clear(); + } - protected override void ManageState(bool isNew) - { - if (isNew) - { - _lastValidValue = Input.Value; - _index++; - } - } - - protected override double Calculation() - { - ManageState(Input.IsNew); - - _buffer.Add(Input.Value, Input.IsNew); - - double variance = 0; - if (_buffer.Count > 1) - { - var values = _buffer.GetSpan().ToArray(); - double mean = values.Average(); - double sumOfSquaredDifferences = values.Sum(x => Math.Pow(x - mean, 2)); - - double divisor = IsPopulation ? _buffer.Count : _buffer.Count - 1; - variance = sumOfSquaredDifferences / divisor; - } - - IsHot = true; - return variance; + /// + /// 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) { + _lastValidValue = Input.Value; + _index++; } } -} \ No newline at end of file + + /// + /// 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. + /// + protected override double Calculation() { + ManageState(Input.IsNew); + + _buffer.Add(Input.Value, Input.IsNew); + + double variance = 0; + if (_buffer.Count > 1) { + var values = _buffer.GetSpan().ToArray(); + double mean = values.Average(); + double sumOfSquaredDifferences = values.Sum(x => Math.Pow(x - mean, 2)); + + double divisor = IsPopulation ? _buffer.Count : _buffer.Count - 1; + variance = sumOfSquaredDifferences / divisor; + } + + IsHot = true; + return variance; + } +} diff --git a/lib/statistics/Zscore.cs b/lib/statistics/Zscore.cs index 6d9a5cee..dafd0f20 100644 --- a/lib/statistics/Zscore.cs +++ b/lib/statistics/Zscore.cs @@ -1,17 +1,27 @@ namespace QuanTAlib; -using System; -using System.Linq; - -public class Zscore : AbstractBase -{ +/// +/// Represents a Z-score calculator that measures how many standard deviations +/// an element is from the mean of a set of values. +/// +/// +/// 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. +/// +public class Zscore : AbstractBase { private readonly int Period; private readonly CircularBuffer _buffer; - public Zscore(int period) : base() - { - if (period < 2) - { + /// + /// 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. + /// + public Zscore(int period) : base() { + if (period < 2) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2 for Z-score calculation."); } Period = period; @@ -21,36 +31,54 @@ public class Zscore : AbstractBase Init(); } - public Zscore(object source, int period) : this(period) - { + /// + /// 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. + public Zscore(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - public override void Init() - { + /// + /// Initializes the Zscore instance by clearing the buffer. + /// + public override void Init() { base.Init(); _buffer.Clear(); } - protected override void ManageState(bool isNew) - { - if (isNew) - { + /// + /// 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) { _lastValidValue = Input.Value; _index++; } } - protected override double Calculation() - { + /// + /// 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. + /// + 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) { // We need at least 2 data points for Z-score var values = _buffer.GetSpan().ToArray(); double mean = values.Average(); double n = values.Length; @@ -58,8 +86,7 @@ public class Zscore : AbstractBase double sumSquaredDeviations = values.Sum(x => Math.Pow(x - mean, 2)); double standardDeviation = Math.Sqrt(sumSquaredDeviations / (n - 1)); // Sample standard deviation - 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 79198654..35a67a15 100644 --- a/lib/volatility/Atr.cs +++ b/lib/volatility/Atr.cs @@ -1,14 +1,26 @@ namespace QuanTAlib; -public class Atr : AbstractBarBase -{ +/// +/// Represents an Average True Range (ATR) calculator, a measure of market volatility. +/// +/// +/// The ATR class calculates the average true range using an Exponential Moving Average (EMA) +/// 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. +/// +public class Atr : AbstractBarBase { private readonly Ema _ma; private double _prevClose, _p_prevClose; - public Atr(int period) : base() - { - if (period < 1) - { + /// + /// 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. + /// + public Atr(int period) { + if (period < 1) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); } _ma = new(1.0/period); @@ -16,34 +28,50 @@ public class Atr : AbstractBarBase Name = $"ATR({period})"; } - public Atr(object source, int period) : this(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. + public Atr(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new BarSignal(Sub)); } - public override void Init() - { + /// + /// Initializes the Atr instance by setting up the initial state. + /// + public override void Init() { base.Init(); _ma.Init(); _prevClose = double.NaN; } - protected override void ManageState(bool isNew) - { - if (isNew) - { + /// + /// 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) { _index++; _p_prevClose = _prevClose; - } - else - { + } else { _prevClose = _p_prevClose; } } - protected override double Calculation() - { + /// + /// 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 EMA + /// 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(Input.IsNew); double trueRange = Math.Max( @@ -53,8 +81,7 @@ public class Atr : AbstractBarBase ), Math.Abs(Input.Low - _prevClose) ); - if (_index < 2) - { + if (_index < 2) { trueRange = Input.High - Input.Low; } @@ -64,6 +91,4 @@ public class Atr : AbstractBarBase return emaTrueRange.Value; } - } - diff --git a/lib/volatility/Historical.cs b/lib/volatility/Historical.cs new file mode 100644 index 00000000..21b80dd8 --- /dev/null +++ b/lib/volatility/Historical.cs @@ -0,0 +1,116 @@ +namespace QuanTAlib; + +/// +/// Represents a historical volatility calculator that measures the dispersion of returns +/// for a given security or market index over a specific period. +/// +/// +/// 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. +/// +public class Historical : AbstractBase { + private readonly int Period; + private readonly bool IsAnnualized; + private readonly CircularBuffer _buffer; + 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. + /// + public Historical(int period, bool isAnnualized = true) : base() { + if (period < 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 + _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). + public Historical(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(); + _buffer.Clear(); + _logReturns.Clear(); + _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) { + _lastValidValue = Input.Value; + _index++; + } + } + + /// + /// 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) { + if (_previousClose != 0) { + double logReturn = Math.Log(Input.Value / _previousClose); + _logReturns.Add(logReturn, Input.IsNew); + } + + 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 + volatility = Math.Sqrt(variance); + + if (IsAnnualized) { + // Assuming 252 trading days in a year. Adjust as needed. + volatility *= Math.Sqrt(252); + } + } + } + + _previousClose = Input.Value; + IsHot = _index >= WarmupPeriod; + return volatility; + } +} diff --git a/lib/volatility/Realized.cs b/lib/volatility/Realized.cs new file mode 100644 index 00000000..ddcd8c76 --- /dev/null +++ b/lib/volatility/Realized.cs @@ -0,0 +1,105 @@ +namespace QuanTAlib; + +/// +/// Represents a realized volatility calculator that measures the actual price fluctuations +/// observed in the market over a specific period. +/// +/// +/// 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. +/// +public class Realized : AbstractBase { + private readonly int Period; + private readonly bool IsAnnualized; + private readonly CircularBuffer _returns; + 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. + /// + public Realized(int period, bool isAnnualized = true) : base() { + if (period < 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 + _returns = new CircularBuffer(period); + Name = $"Realized(period={period}, annualized={isAnnualized})"; + Init(); + } + + /// + /// Initializes the Realized instance by clearing buffers and resetting calculation variables. + /// + public override void Init() { + base.Init(); + _returns.Clear(); + _previousClose = 0; + _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) { + _lastValidValue = Input.Value; + _index++; + } + } + + /// + /// 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); + + double volatility = 0; + if (_previousClose != 0) { + double logReturn = Math.Log(Input.Value / _previousClose); + + if (_returns.Count == Period) { + // Remove the oldest squared return from the sum + _sumSquaredReturns -= Math.Pow(_returns[0], 2); + } + + _returns.Add(logReturn, Input.IsNew); + _sumSquaredReturns += Math.Pow(logReturn, 2); + + if (_returns.Count == Period) { + double variance = _sumSquaredReturns / Period; + volatility = Math.Sqrt(variance); + + if (IsAnnualized) { + // Assuming 252 trading days in a year. Adjust as needed. + volatility *= Math.Sqrt(252); + } + } + } + + _previousClose = Input.Value; + IsHot = _index >= WarmupPeriod; + return volatility; + } +} \ No newline at end of file diff --git a/lib/volatility/Rvi.cs b/lib/volatility/Rvi.cs new file mode 100644 index 00000000..ee3349ea --- /dev/null +++ b/lib/volatility/Rvi.cs @@ -0,0 +1,110 @@ +namespace QuanTAlib; + +/// +/// Represents a Relative Volatility Index (RVI) calculator, which measures the direction +/// of volatility in relation to price movements. +/// +/// +/// 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. +/// +/// This implementation uses a combination of Standard Deviation and Simple Moving Average +/// calculations to compute the RVI. +/// +public class Rvi : AbstractBase { + private readonly int Period; + private Stddev _upStdDev, _downStdDev; + private 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. + /// + public Rvi(int period) : base() { + if (period < 2) { + throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2."); + } + Period = period; + WarmupPeriod = period; + Name = $"RVI(period={period})"; + _upStdDev = new Stddev(Period); + _downStdDev = new Stddev(Period); + _upSma = new(Period); + _downSma = new(Period); + 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. + 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) { + _lastValidValue = Value; + _index++; + } + } + + /// + /// 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); + + double close = Input.Value; + double change = close - _previousClose; + + double upMove = Math.Max(change, 0); + double downMove = Math.Max(-change, 0); + + _upSma.Calc(_upStdDev.Calc(new TValue(Input.Time, upMove, Input.IsNew))); + _downSma.Calc(_downStdDev.Calc(new TValue(Input.Time, downMove, Input.IsNew))); + + double rvi; + if (_upSma.Value + _downSma.Value != 0) { + rvi = 100 * _upSma.Value / (_upSma.Value + _downSma.Value); + } else { + rvi = 0; + } + + _previousClose = close; + IsHot = _index >= WarmupPeriod; + return rvi; + } +} diff --git a/lib/volatility/todo.md b/lib/volatility/todo.md new file mode 100644 index 00000000..512a9e63 --- /dev/null +++ b/lib/volatility/todo.md @@ -0,0 +1,28 @@ +Single Value Input (Typically Closing Prices) + +Jurik Volatility (Volty) + +**Standard Deviation** +**Relative Volatility Index (RVI)** +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 + +OHLC Input (Open, High, Low, Close) + +Garman-Klass Volatility +Rogers-Satchell Volatility +Yang-Zhang Volatility +Parkinson Volatility (High, Low) +Average True Range (ATR) (High, Low, Close) +Chaikin Volatility (High, Low) +Keltner Channels (typically Close, High, Low) +High-Low Volatility (High, Low) + diff --git a/quantower/Volatility/HistoricalIndicator.cs b/quantower/Volatility/HistoricalIndicator.cs new file mode 100644 index 00000000..9196da81 --- /dev/null +++ b/quantower/Volatility/HistoricalIndicator.cs @@ -0,0 +1,28 @@ +using TradingPlatform.BusinessLayer; +namespace QuanTAlib; + +public class HistoricalIndicator : IndicatorBase +{ + [InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)] + public int Period { get; set; } = 20; + + [InputParameter("Annualized", sortIndex: 2)] + public bool IsAnnualized { get; set; } = true; + + private Historical? historical; + protected override AbstractBase QuanTAlib => historical!; + public override string ShortName => $"Historical Volatility {Period}{(IsAnnualized ? " - Annualized" : "")} : {SourceName}"; + + public HistoricalIndicator() : base() + { + Name = "HV - Historical Volatility"; + SeparateWindow = true; + } + + protected override void InitIndicator() + { + historical = new(Period, IsAnnualized); + MinHistoryDepths = historical.WarmupPeriod; + base.InitIndicator(); + } +} \ No newline at end of file diff --git a/quantower/Volatility/RealizedIndicator.cs b/quantower/Volatility/RealizedIndicator.cs new file mode 100644 index 00000000..44039edd --- /dev/null +++ b/quantower/Volatility/RealizedIndicator.cs @@ -0,0 +1,28 @@ +using TradingPlatform.BusinessLayer; +namespace QuanTAlib; + +public class RealizedIndicator : IndicatorBase +{ + [InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)] + public int Period { get; set; } = 20; + + [InputParameter("Annualized", sortIndex: 2)] + public bool IsAnnualized { get; set; } = true; + + private Realized? realized; + protected override AbstractBase QuanTAlib => realized!; + public override string ShortName => $"Realized Volatility {Period}{(IsAnnualized ? " - Annualized" : "")} : {SourceName}"; + + public RealizedIndicator() : base() + { + Name = "RV - Realized Volatility"; + SeparateWindow = true; + } + + protected override void InitIndicator() + { + realized = new(Period, IsAnnualized); + MinHistoryDepths = realized.WarmupPeriod; + base.InitIndicator(); + } +} \ No newline at end of file diff --git a/quantower/Volatility/RviIndicator.cs b/quantower/Volatility/RviIndicator.cs new file mode 100644 index 00000000..7e85d13a --- /dev/null +++ b/quantower/Volatility/RviIndicator.cs @@ -0,0 +1,29 @@ +using TradingPlatform.BusinessLayer; +namespace QuanTAlib; + +public class RviIndicator : IndicatorBase +{ + [InputParameter("Period", sortIndex: 1, 2, 100, 1, 0)] + public int Period { get; set; } = 10; + + private Rvi? rvi; + protected override AbstractBase QuanTAlib => rvi!; + public override string ShortName => $"RVI {Period} : {SourceName}"; + + public RviIndicator() : base() + { + Name = "RVI - Relative Volatility Index"; + SeparateWindow = true; + + // Adding upper and lower reference lines + //AddLineSeries("UpperLevel", 80, System.Drawing.Color.Gray, 1, LineStyle.Dot); + //AddLineSeries("LowerLevel", 20, System.Drawing.Color.Gray, 1, LineStyle.Dot); + } + + protected override void InitIndicator() + { + rvi = new Rvi(Period); + MinHistoryDepths = rvi.WarmupPeriod; + base.InitIndicator(); + } +} diff --git a/quantower/_IndicatorBase.cs b/quantower/_IndicatorBase.cs index 043fd9e1..d1805336 100644 --- a/quantower/_IndicatorBase.cs +++ b/quantower/_IndicatorBase.cs @@ -27,7 +27,7 @@ public abstract class IndicatorBase : Indicator, IWatchlistIndicator [InputParameter("Show cold values", sortIndex: 20)] public bool ShowColdValues { get; set; } = true; - public int MinHistoryDepths; + public int MinHistoryDepths { get; set; }; // LineSeries.LineSeries(string, Color, int, LineStyle)' @@ -37,7 +37,7 @@ public abstract class IndicatorBase : Indicator, IWatchlistIndicator int IWatchlistIndicator.MinHistoryDepths => 0; - protected IndicatorBase() : base() + protected IndicatorBase() { OnBackGround = true; SeparateWindow = false; @@ -45,7 +45,6 @@ public abstract class IndicatorBase : Indicator, IWatchlistIndicator Series = new(name: $"{Name}", color: Color.Yellow, width: 2, style: LineStyle.Solid); AddLineSeries(Series); - InitIndicator(); } protected virtual void InitIndicator() @@ -95,7 +94,7 @@ public abstract class IndicatorBase : Indicator, IWatchlistIndicator { base.OnPaintChart(args); List allPoints = new List(); - if (CurrentChart == null) return; + if (CurrentChart == null) { return }; Graphics gr = args.Graphics; var mainWindow = this.CurrentChart.Windows[args.WindowIndex]; @@ -126,7 +125,7 @@ public abstract class IndicatorBase : Indicator, IWatchlistIndicator private void DrawSmoothCombinedCurve(Graphics gr, List allPoints, int hotCount) { - if (allPoints.Count < 2) return; + if (allPoints.Count < 2) { return }; using (Pen defaultPen = new(Series!.Color, Series.Width) { DashStyle = ConvertLineStyleToDashStyle(Series.Style) }) using (Pen coldPen = new(Series!.Color, Series.Width) { DashStyle = DashStyle.Dot })