diff --git a/Tests/test_eventing.cs b/Tests/test_eventing.cs index 5406876e..b0689b4d 100644 --- a/Tests/test_eventing.cs +++ b/Tests/test_eventing.cs @@ -76,6 +76,8 @@ public class EventingTests ("Stddev", new Stddev(p), new Stddev(input, p)), ("Variance", new Variance(p), new Variance(input, p)), ("Zscore", new Zscore(p), new Zscore(input, p)), + ("Beta", new Beta(p), new Beta(input, p)), + ("Corr", new Corr(p), new Corr(input, p)), // Volatility indicators (value-based) ("Hv", new Hv(p), new Hv(input, p)), ("Jvolty", new Jvolty(p), new Jvolty(input, p)), diff --git a/Tests/test_updates_statistics.cs b/Tests/test_updates_statistics.cs index e1c902aa..ff6ed756 100644 --- a/Tests/test_updates_statistics.cs +++ b/Tests/test_updates_statistics.cs @@ -26,6 +26,39 @@ public class StatisticsUpdateTests return new TBar(DateTime.Now, open, high, low, close, 1000, IsNew); } + [Fact] + public void Beta_Update() + { + var indicator = new Beta(period: 14); + TBar marketBar = GetRandomBar(true); + TBar assetBar = GetRandomBar(true); + double initialValue = indicator.Calc(marketBar, assetBar); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(false), GetRandomBar(false)); + } + double finalValue = indicator.Calc(new TBar(marketBar.Time, marketBar.Open, marketBar.High, marketBar.Low, marketBar.Close, marketBar.Volume, false), + new TBar(assetBar.Time, assetBar.Open, assetBar.High, assetBar.Low, assetBar.Close, assetBar.Volume, false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Corr_Update() + { + var indicator = new Corr(period: 14); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true), new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false), new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false), new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + [Fact] public void Curvature_Update() { diff --git a/docs/indicators/indicators.md b/docs/indicators/indicators.md index a88d07f9..4fdc43aa 100644 --- a/docs/indicators/indicators.md +++ b/docs/indicators/indicators.md @@ -6,11 +6,12 @@ | Averages & Trends | 33 of 33 | 100% | | Momentum | 16 of 16 | 100% | | Oscillators | 22 of 29 | 76% | -| Volatility | 24 of 35 | 69% | -| Volume | 15 of 19 | 79% | -| Numerical Analysis | 13 of 19 | 68% | +| Volatility | 29 of 35 | 83% | +| Volume | 19 of 19 | 100% | +| Numerical Analysis | 15 of 19 | 79% | | Errors | 16 of 16 | 100% | -| **Total** | **145 of 173** | **84%** | +| Patterns | 0 of 8 | 0% | +| **Total** | **156 of 181** | **86%** | |Technical Indicator Name| Class Name| |-----------|:----------:| @@ -85,9 +86,10 @@ |COPPOCK - Coppock Curve|`Coppock`| |CRSI - Connor RSI|`Crsi`| |🚧 CTI - Ehler's Correlation Trend Indicator|`Cti`| +|DOSC - Derivative Oscillator|`Dosc`| +|EFI - Elder Ray's Force Index|`Efi`| |🚧 FISHER - Fisher Transform|`Fisher`| |🚧 FOSC - Forecast Oscillator|`Fosc`| -|EFI - Elder Ray's Force Index|`Efi`| |🚧 GATOR* - Williams Alliator Oscillator (Upper Jaw, Lower Jaw, Teeth)|`Gator`| |🚧 KDJ* - KDJ Indicator (K, D, J lines)|`Kdj`| |🚧 KRI - Kairi Relative Index|`Kri`| @@ -101,7 +103,15 @@ |TSI - True Strength Index|`Tsi`| |UO - Ultimate Oscillator|`Uo`| |WILLR - Larry Williams' %R|`Willr`| -|DOSC - Derivative Oscillator|`Dosc`| +|**PATTERNS**|| +|🚧 DOJI - Doji Candlestick Pattern|`Doji`| +|🚧 ER* - Elder Ray Pattern (Bull Power, Bear Power)|`Er`| +|🚧 MARU - Marubozu Candlestick Pattern|`Maru`| +|🚧 PIV* - Pivot Points (Support 1-3, Pivot, Resistance 1-3)|`Piv`| +|🚧 PP* - Price Pivots (Support 1-3, Pivot, Resistance 1-3)|`Pp`| +|🚧 RPP* - Rolling Pivot Points (Support 1-3, Pivot, Resistance 1-3)|`Rpp`| +|🚧 WF - Williams Fractal|`Wf`| +|🚧 ZZ - Zig Zag Pattern|`Zz`| |**VOLATILITY INDICATORS**|| |ADR - Average Daily Range|`Adr`| |AP - Andrew's Pitchfork|`Ap`| @@ -159,12 +169,12 @@ |VWAP - Volume Weighted Average Price|`Vwap`| |VWMA - Volume Weighted Moving Average|`Vwma`| |**NUMERICAL ANALYSIS**|| -|🚧 BETA* - Beta coefficient (Beta, R-squared)|`Beta`| -|🚧 CORR* - Correlation Coefficient (Correlation, P-value)|`Corr`| +|BETA* - Beta coefficient (Beta, R-squared)|`Beta`| +|CORR* - Correlation Coefficient (Correlation, P-value)|`Corr`| |CURVATURE - Rate of Change in Direction or Slope|`Curvature`| |ENTROPY - Measure of Uncertainty or Disorder|`Entropy`| -|🚧 HUBER - Huber Loss|`Huber`| -|🚧 HURST - Hurst Exponent|`Hurst`| +|HUBER - Huber Loss|`Huber`| +|HURST - Hurst Exponent|`Hurst`| |KURTOSIS - Measure of Tails/Peakedness|`Kurtosis`| |MAX - Maximum with exponential decay|`Max`| |MEDIAN - Middle value|`Median`| diff --git a/lib/errors/Huber.cs b/lib/errors/Huber.cs index 12c5779b..ffe7a0a1 100644 --- a/lib/errors/Huber.cs +++ b/lib/errors/Huber.cs @@ -43,14 +43,9 @@ public sealed class Huber : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public Huber(int period, double delta = 1.0) { - if (period < 1) - { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); - } - if (delta <= 0) - { - throw new ArgumentOutOfRangeException(nameof(delta), "Delta must be greater than 0."); - } + ArgumentOutOfRangeException.ThrowIfLessThan(period, 1); + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(delta, 0); + WarmupPeriod = period; _actualBuffer = new CircularBuffer(period); _predictedBuffer = new CircularBuffer(period); diff --git a/lib/momentum/Macd.cs b/lib/momentum/Macd.cs index 67c4d589..263c80ec 100644 --- a/lib/momentum/Macd.cs +++ b/lib/momentum/Macd.cs @@ -60,14 +60,14 @@ public sealed class Macd : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public Macd(int fastPeriod = DefaultFastPeriod, int slowPeriod = DefaultSlowPeriod, int signalPeriod = DefaultSignalPeriod) { - if (fastPeriod < 1) - throw new ArgumentOutOfRangeException(nameof(fastPeriod)); - if (slowPeriod < 1) - throw new ArgumentOutOfRangeException(nameof(slowPeriod)); - if (signalPeriod < 1) - throw new ArgumentOutOfRangeException(nameof(signalPeriod)); + ArgumentOutOfRangeException.ThrowIfLessThan(fastPeriod, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(slowPeriod, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(signalPeriod, 1); + if (fastPeriod >= slowPeriod) - throw new ArgumentException("Fast period must be less than slow period"); + { + throw new ArgumentOutOfRangeException(nameof(fastPeriod), "Fast period must be less than slow period"); + } _fastEma = new(fastPeriod); _slowEma = new(slowPeriod); diff --git a/lib/momentum/_list.md b/lib/momentum/_list.md index 1d01f144..85cc2072 100644 --- a/lib/momentum/_list.md +++ b/lib/momentum/_list.md @@ -4,7 +4,7 @@ Done: 15, Todo: 2 ✔️ ADX - Average Directional Movement Index ✔️ ADXR - Average Directional Movement Index Rating ✔️ APO - Absolute Price Oscillator -✔️ *DMI - Directional Movement Index (DI+, DI-) +✔️ DMI - Directional Movement Index (DI+, DI-) ✔️ DMX - Jurik Directional Movement Index ✔️ DPO - Detrended Price Oscillator ✔️ *MACD - Moving Average Convergence/Divergence (MACD, Signal, Histogram) diff --git a/lib/oscillators/Coppock.cs b/lib/oscillators/Coppock.cs index 9cd6231c..ed905b4c 100644 --- a/lib/oscillators/Coppock.cs +++ b/lib/oscillators/Coppock.cs @@ -51,12 +51,9 @@ public sealed class Coppock : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public Coppock(int roc1Period = DefaultRoc1Period, int roc2Period = DefaultRoc2Period, int wmaPeriod = DefaultWmaPeriod) { - if (roc1Period < 1) - throw new ArgumentOutOfRangeException(nameof(roc1Period), "ROC1 period must be greater than 0"); - if (roc2Period < 1) - throw new ArgumentOutOfRangeException(nameof(roc2Period), "ROC2 period must be greater than 0"); - if (wmaPeriod < 1) - throw new ArgumentOutOfRangeException(nameof(wmaPeriod), "WMA period must be greater than 0"); + ArgumentOutOfRangeException.ThrowIfLessThan(roc1Period, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(roc2Period, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(wmaPeriod, 1); _roc1Period = roc1Period; _roc2Period = roc2Period; diff --git a/lib/oscillators/Rsi.cs b/lib/oscillators/Rsi.cs index c18f70aa..3db9f953 100644 --- a/lib/oscillators/Rsi.cs +++ b/lib/oscillators/Rsi.cs @@ -48,8 +48,7 @@ public sealed class Rsi : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rsi(int period = DefaultPeriod) { - if (period < 1) - throw new ArgumentOutOfRangeException(nameof(period)); + ArgumentOutOfRangeException.ThrowIfLessThan(period, 1); _avgGain = new(period, useSma: true); _avgLoss = new(period, useSma: true); _index = 0; diff --git a/lib/oscillators/Smi.cs b/lib/oscillators/Smi.cs index 27e6e635..989218d1 100644 --- a/lib/oscillators/Smi.cs +++ b/lib/oscillators/Smi.cs @@ -57,12 +57,9 @@ public sealed class Smi : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public Smi(int period = DefaultPeriod, int smooth1 = DefaultSmooth1, int smooth2 = DefaultSmooth2) { - if (period < 1) - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0"); - if (smooth1 < 1) - throw new ArgumentOutOfRangeException(nameof(smooth1), "Smooth1 must be greater than 0"); - if (smooth2 < 1) - throw new ArgumentOutOfRangeException(nameof(smooth2), "Smooth2 must be greater than 0"); + ArgumentOutOfRangeException.ThrowIfLessThan(period, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(smooth1, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(smooth2, 1); _highs = new(period); _lows = new(period); diff --git a/lib/oscillators/Srsi.cs b/lib/oscillators/Srsi.cs index c58d3fe9..bdb2c3df 100644 --- a/lib/oscillators/Srsi.cs +++ b/lib/oscillators/Srsi.cs @@ -40,7 +40,6 @@ public sealed class Srsi : AbstractBase private readonly CircularBuffer _srsiValues; private readonly Sma _signal; private readonly int _rsiPeriod; - private readonly int _stochPeriod; private const int DefaultRsiPeriod = 14; private const int DefaultStochPeriod = 14; private const int DefaultSmoothK = 3; @@ -56,25 +55,12 @@ public sealed class Srsi : AbstractBase public Srsi(int rsiPeriod = DefaultRsiPeriod, int stochPeriod = DefaultStochPeriod, int smoothK = DefaultSmoothK, int smoothD = DefaultSmoothD) { - if (rsiPeriod < 1) - { - throw new ArgumentOutOfRangeException(nameof(rsiPeriod), "Period must be greater than 0"); - } - if (stochPeriod < 1) - { - throw new ArgumentOutOfRangeException(nameof(stochPeriod), "Period must be greater than 0"); - } - if (smoothK < 1) - { - throw new ArgumentOutOfRangeException(nameof(smoothK), "Period must be greater than 0"); - } - if (smoothD < 1) - { - throw new ArgumentOutOfRangeException(nameof(smoothD), "Period must be greater than 0"); - } + ArgumentOutOfRangeException.ThrowIfLessThan(rsiPeriod, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(stochPeriod, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(smoothK, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(smoothD, 1); _rsiPeriod = rsiPeriod; - _stochPeriod = stochPeriod; _rsi = new(rsiPeriod); _rsiValues = new(stochPeriod); _srsiValues = new(smoothK); diff --git a/lib/oscillators/Stc.cs b/lib/oscillators/Stc.cs index b1ab2560..8b755952 100644 --- a/lib/oscillators/Stc.cs +++ b/lib/oscillators/Stc.cs @@ -62,32 +62,17 @@ public sealed class Stc : AbstractBase int slowPeriod = DefaultSlowPeriod, int d1Period = DefaultD1Period, int stcPeriod = DefaultStcPeriod) { - string err = "All periods must be greater than 0"; + ArgumentOutOfRangeException.ThrowIfLessThan(cyclePeriod, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(fastPeriod, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(slowPeriod, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(d1Period, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(stcPeriod, 1); - if (cyclePeriod < 1) - { - throw new ArgumentOutOfRangeException(nameof(cyclePeriod), err); - } - if (fastPeriod < 1) - { - throw new ArgumentOutOfRangeException(nameof(fastPeriod), err); - } - if (slowPeriod < 1) - { - throw new ArgumentOutOfRangeException(nameof(slowPeriod), err); - } - if (d1Period < 1) - { - throw new ArgumentOutOfRangeException(nameof(d1Period), err); - } - if (stcPeriod < 1) - { - throw new ArgumentOutOfRangeException(nameof(stcPeriod), err); - } if (fastPeriod >= slowPeriod) { throw new ArgumentOutOfRangeException(nameof(fastPeriod), "Fast period must be less than slow period"); } + _fastEma = new(fastPeriod); _slowEma = new(slowPeriod); _macdValues = new(cyclePeriod); diff --git a/lib/oscillators/Stoch.cs b/lib/oscillators/Stoch.cs index ab10d0b6..1f5d30a0 100644 --- a/lib/oscillators/Stoch.cs +++ b/lib/oscillators/Stoch.cs @@ -52,12 +52,9 @@ public sealed class Stoch : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public Stoch(int period = DefaultPeriod, int smoothK = DefaultSmoothK, int smoothD = DefaultSmoothD) { - if (period < 1) - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0"); - if (smoothK < 1) - throw new ArgumentOutOfRangeException(nameof(smoothK), "%K smoothing period must be greater than 0"); - if (smoothD < 1) - throw new ArgumentOutOfRangeException(nameof(smoothD), "%D smoothing period must be greater than 0"); + ArgumentOutOfRangeException.ThrowIfLessThan(period, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(smoothK, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(smoothD, 1); _highs = new(period); _lows = new(period); diff --git a/lib/oscillators/Uo.cs b/lib/oscillators/Uo.cs index ec247ebd..38a98129 100644 --- a/lib/oscillators/Uo.cs +++ b/lib/oscillators/Uo.cs @@ -67,30 +67,12 @@ public sealed class Uo : AbstractBase public Uo(int period1 = DefaultPeriod1, int period2 = DefaultPeriod2, int period3 = DefaultPeriod3, double weight1 = DefaultWeight1, double weight2 = DefaultWeight2, double weight3 = DefaultWeight3) { - if (period1 < 1) - { - throw new ArgumentOutOfRangeException(nameof(period1), "Period1 must be greater than 0"); - } - if (period2 < 1) - { - throw new ArgumentOutOfRangeException(nameof(period2), "Period2 must be greater than 0"); - } - if (period3 < 1) - { - throw new ArgumentOutOfRangeException(nameof(period3), "Period3 must be greater than 0"); - } - if (weight1 <= 0) - { - throw new ArgumentOutOfRangeException(nameof(weight1), "Weight1 must be greater than 0"); - } - if (weight2 <= 0) - { - throw new ArgumentOutOfRangeException(nameof(weight2), "Weight2 must be greater than 0"); - } - if (weight3 <= 0) - { - throw new ArgumentOutOfRangeException(nameof(weight3), "Weight3 must be greater than 0"); - } + ArgumentOutOfRangeException.ThrowIfLessThan(period1, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(period2, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(period3, 1); + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(weight1, 0); + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(weight2, 0); + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(weight3, 0); _weight1 = weight1; _weight2 = weight2; diff --git a/lib/statistics/Beta.cs b/lib/statistics/Beta.cs new file mode 100644 index 00000000..97e065f2 --- /dev/null +++ b/lib/statistics/Beta.cs @@ -0,0 +1,159 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// BETA: Beta Coefficient +/// A statistical measure that quantifies the volatility of an asset or portfolio +/// in relation to the overall market. Beta is used to assess the risk and return +/// characteristics of an investment. +/// +/// +/// The Beta calculation process: +/// 1. Calculates covariance between asset and market returns +/// 2. Computes variance of market returns +/// 3. Divides covariance by market variance +/// +/// Key characteristics: +/// - Measures relative volatility +/// - Beta > 1: More volatile than market +/// - Beta < 1: Less volatile than market +/// - Beta = 1: Same volatility as market +/// - Beta < 0: Inverse relationship with market +/// +/// Formula: +/// β = Cov(Ra, Rm) / Var(Rm) +/// where: +/// Ra = asset returns +/// Rm = market returns +/// +/// Market Applications: +/// - Risk assessment +/// - Portfolio management +/// - Asset allocation +/// - Performance analysis +/// - Hedging strategies +/// +/// Sources: +/// https://en.wikipedia.org/wiki/Beta_(finance) +/// "Modern Portfolio Theory" - Harry Markowitz +/// +/// Note: Assumes linear relationship between asset and market returns +/// +[SkipLocalsInit] +public sealed class Beta : AbstractBase +{ + private readonly int Period; + private readonly CircularBuffer _assetReturns; + private readonly CircularBuffer _marketReturns; + private const double Epsilon = 1e-10; + private const int MinimumPoints = 2; + + /// The number of points to consider for beta calculation. + /// Thrown when period is less than 2. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Beta(int period) + { + if (period < MinimumPoints) + { + throw new ArgumentOutOfRangeException(nameof(period), + "Period must be greater than or equal to 2 for beta calculation."); + } + Period = period; + WarmupPeriod = MinimumPoints; + _assetReturns = new CircularBuffer(period); + _marketReturns = new CircularBuffer(period); + Name = $"Beta(period={period})"; + Init(); + } + + /// The data source object that publishes updates. + /// The number of points to consider for beta calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Beta(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _assetReturns.Clear(); + _marketReturns.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Input.Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateMean(ReadOnlySpan values) + { + double sum = 0; + for (int i = 0; i < values.Length; i++) + { + sum += values[i]; + } + return sum / values.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateCovariance(ReadOnlySpan assetReturns, ReadOnlySpan marketReturns, double assetMean, double marketMean) + { + double covariance = 0; + for (int i = 0; i < assetReturns.Length; i++) + { + covariance += (assetReturns[i] - assetMean) * (marketReturns[i] - marketMean); + } + return covariance / assetReturns.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateVariance(ReadOnlySpan values, double mean) + { + double variance = 0; + for (int i = 0; i < values.Length; i++) + { + double diff = values[i] - mean; + variance += diff * diff; + } + return variance / values.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + _assetReturns.Add(Input.Value, Input.IsNew); + _marketReturns.Add(Input2.Value, Input.IsNew); + + double beta = 0; + if (_assetReturns.Count >= MinimumPoints && _marketReturns.Count >= MinimumPoints) + { + ReadOnlySpan assetValues = _assetReturns.GetSpan(); + ReadOnlySpan marketValues = _marketReturns.GetSpan(); + + double assetMean = CalculateMean(assetValues); + double marketMean = CalculateMean(marketValues); + + double covariance = CalculateCovariance(assetValues, marketValues, assetMean, marketMean); + double marketVariance = CalculateVariance(marketValues, marketMean); + + if (marketVariance > Epsilon) + { + beta = covariance / marketVariance; + } + } + + IsHot = _assetReturns.Count >= Period && _marketReturns.Count >= Period; + return beta; + } +} diff --git a/lib/statistics/Corr.cs b/lib/statistics/Corr.cs new file mode 100644 index 00000000..0eb18c50 --- /dev/null +++ b/lib/statistics/Corr.cs @@ -0,0 +1,163 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// CORR: Correlation Coefficient +/// A statistical measure that quantifies the strength and direction of the relationship +/// between two variables. The correlation coefficient ranges from -1 to 1, where 1 indicates +/// a perfect positive correlation, -1 indicates a perfect negative correlation, and 0 indicates +/// no correlation. +/// +/// +/// The Correlation calculation process: +/// 1. Calculates mean of both variables +/// 2. Computes covariance between variables +/// 3. Calculates standard deviation of both variables +/// 4. Divides covariance by product of standard deviations +/// +/// Key characteristics: +/// - Measures linear relationship strength +/// - Symmetric around zero +/// - Scale-independent measure +/// - Sensitive to outliers +/// - Useful for portfolio diversification +/// +/// Formula: +/// ρ = Cov(X, Y) / (σX * σY) +/// where: +/// X, Y = variables +/// Cov = covariance +/// σ = standard deviation +/// +/// Market Applications: +/// - Portfolio diversification +/// - Risk management +/// - Pairs trading +/// - Performance analysis +/// - Market sentiment analysis +/// +/// Sources: +/// https://en.wikipedia.org/wiki/Correlation_coefficient +/// "Modern Portfolio Theory" - Harry Markowitz +/// +/// Note: Assumes linear relationship between variables +/// +[SkipLocalsInit] +public sealed class Corr : AbstractBase +{ + private readonly int Period; + private readonly CircularBuffer _xValues; + private readonly CircularBuffer _yValues; + private const double Epsilon = 1e-10; + private const int MinimumPoints = 2; + + /// The number of points to consider for correlation calculation. + /// Thrown when period is less than 2. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Corr(int period) + { + if (period < MinimumPoints) + { + throw new ArgumentOutOfRangeException(nameof(period), + "Period must be greater than or equal to 2 for correlation calculation."); + } + Period = period; + WarmupPeriod = MinimumPoints; + _xValues = new CircularBuffer(period); + _yValues = new CircularBuffer(period); + Name = $"Corr(period={period})"; + Init(); + } + + /// The data source object that publishes updates. + /// The number of points to consider for correlation calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Corr(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _xValues.Clear(); + _yValues.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Input.Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateMean(ReadOnlySpan values) + { + double sum = 0; + for (int i = 0; i < values.Length; i++) + { + sum += values[i]; + } + return sum / values.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateCovariance(ReadOnlySpan xValues, ReadOnlySpan yValues, double xMean, double yMean) + { + double covariance = 0; + for (int i = 0; i < xValues.Length; i++) + { + covariance += (xValues[i] - xMean) * (yValues[i] - yMean); + } + return covariance / xValues.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateStandardDeviation(ReadOnlySpan values, double mean) + { + double sumSquaredDeviations = 0; + for (int i = 0; i < values.Length; i++) + { + double deviation = values[i] - mean; + sumSquaredDeviations += deviation * deviation; + } + return Math.Sqrt(sumSquaredDeviations / values.Length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + _xValues.Add(Input.Value, Input.IsNew); + _yValues.Add(Input2.Value, Input.IsNew); + + double correlation = 0; + if (_xValues.Count >= MinimumPoints && _yValues.Count >= MinimumPoints) + { + ReadOnlySpan xValues = _xValues.GetSpan(); + ReadOnlySpan yValues = _yValues.GetSpan(); + + double xMean = CalculateMean(xValues); + double yMean = CalculateMean(yValues); + + double covariance = CalculateCovariance(xValues, yValues, xMean, yMean); + double xStdDev = CalculateStandardDeviation(xValues, xMean); + double yStdDev = CalculateStandardDeviation(yValues, yMean); + + if (xStdDev > Epsilon && yStdDev > Epsilon) + { + correlation = covariance / (xStdDev * yStdDev); + } + } + + IsHot = _xValues.Count >= Period && _yValues.Count >= Period; + return correlation; + } +} diff --git a/lib/statistics/Percentile.cs b/lib/statistics/Percentile.cs index 635f6680..c8ef7be9 100644 --- a/lib/statistics/Percentile.cs +++ b/lib/statistics/Percentile.cs @@ -45,7 +45,6 @@ public sealed class Percentile : AbstractBase private readonly int Period; private readonly double Percent; private readonly CircularBuffer _buffer; - private const double Epsilon = 1e-10; private const int MinimumPoints = 2; /// The number of points to consider for percentile calculation. @@ -56,16 +55,10 @@ public sealed class Percentile : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public Percentile(int period, double percent) { - if (period < MinimumPoints) - { - throw new ArgumentOutOfRangeException(nameof(period), - "Period must be greater than or equal to 2 for percentile calculation."); - } - if (percent < 0 || percent > 100) - { - throw new ArgumentOutOfRangeException(nameof(percent), - "Percent must be between 0 and 100."); - } + ArgumentOutOfRangeException.ThrowIfLessThan(period, MinimumPoints); + ArgumentOutOfRangeException.ThrowIfLessThan(percent, 0); + ArgumentOutOfRangeException.ThrowIfGreaterThan(percent, 100); + Period = period; Percent = percent; WarmupPeriod = MinimumPoints; // Minimum number of points needed for percentile calculation diff --git a/lib/statistics/Theil.cs b/lib/statistics/Theil.cs new file mode 100644 index 00000000..3ebcb7b5 --- /dev/null +++ b/lib/statistics/Theil.cs @@ -0,0 +1,167 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// THEIL: Theil's U Statistics (U1, U2) +/// A statistical measure that quantifies the accuracy of forecasts compared to actual values +/// and naive forecasts. +/// +/// +/// The Theil's U calculation process: +/// 1. Calculate U1 statistic (relative accuracy) +/// 2. Calculate U2 statistic (comparison with naive forecast) +/// +/// Key characteristics: +/// - U1 ranges from 0 to 1, with 0 indicating perfect forecast +/// - U2 < 1: forecast better than naive forecast +/// - U2 = 1: forecast equal to naive forecast +/// - U2 > 1: forecast worse than naive forecast +/// +/// Formula: +/// U1 = √[Σ(Ft - At)² / Σ(At)²] +/// U2 = √[Σ(Ft - At)² / Σ(At - At-1)²] +/// where: +/// Ft = forecasted value +/// At = actual value +/// At-1 = previous actual value +/// +/// Market Applications: +/// - Evaluating forecast accuracy +/// - Comparing forecasting models +/// - Assessing forecasting methods +/// - Model selection +/// - Performance analysis +/// +/// Sources: +/// https://en.wikipedia.org/wiki/Theil%27s_U +/// "Forecasting: Principles and Practice" - Rob J Hyndman +/// +/// Note: Should be used alongside other accuracy measures +/// +[SkipLocalsInit] +public sealed class Theil : AbstractBase +{ + private readonly int Period; + private readonly CircularBuffer _actual; + private readonly CircularBuffer _forecast; + private const int MinimumPoints = 2; + + /// + /// Gets the U2 statistic comparing forecast with naive forecast + /// + public double U2 { get; private set; } + + /// The number of points to consider for Theil's U calculation. + /// Thrown when period is less than 2. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Theil(int period) + { + if (period < MinimumPoints) + { + throw new ArgumentOutOfRangeException(nameof(period), + "Period must be greater than or equal to 2 for Theil's U calculation."); + } + Period = period; + WarmupPeriod = MinimumPoints; + _actual = new CircularBuffer(period); + _forecast = new CircularBuffer(period); + Name = $"Theil(period={period})"; + Init(); + } + + /// The data source object that publishes updates. + /// The number of points to consider for Theil's U calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Theil(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _actual.Clear(); + _forecast.Clear(); + U2 = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Input.Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateSquaredSum(ReadOnlySpan values) + { + double sum = 0; + for (int i = 0; i < values.Length; i++) + { + sum += values[i] * values[i]; + } + return sum; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateSquaredErrorSum(ReadOnlySpan forecast, ReadOnlySpan actual) + { + double sum = 0; + for (int i = 0; i < forecast.Length; i++) + { + double error = forecast[i] - actual[i]; + sum += error * error; + } + return sum; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateNaiveSquaredErrorSum(ReadOnlySpan actual) + { + double sum = 0; + for (int i = 1; i < actual.Length; i++) + { + double error = actual[i] - actual[i - 1]; + sum += error * error; + } + return sum; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + _actual.Add(Input.Value, Input.IsNew); + _forecast.Add(Input2.Value, Input.IsNew); + + double u1 = 0; + if (_actual.Count >= MinimumPoints && _forecast.Count >= MinimumPoints) + { + ReadOnlySpan actualValues = _actual.GetSpan(); + ReadOnlySpan forecastValues = _forecast.GetSpan(); + + double squaredErrorSum = CalculateSquaredErrorSum(forecastValues, actualValues); + double squaredActualSum = CalculateSquaredSum(actualValues); + double naiveSquaredErrorSum = CalculateNaiveSquaredErrorSum(actualValues); + + if (squaredActualSum > double.Epsilon) + { + u1 = Math.Sqrt(squaredErrorSum / squaredActualSum); + } + + if (naiveSquaredErrorSum > double.Epsilon) + { + U2 = Math.Sqrt(squaredErrorSum / naiveSquaredErrorSum); + } + } + + IsHot = _actual.Count >= Period && _forecast.Count >= Period; + return u1; + } +} diff --git a/lib/statistics/Tsf.cs b/lib/statistics/Tsf.cs new file mode 100644 index 00000000..7001e1e0 --- /dev/null +++ b/lib/statistics/Tsf.cs @@ -0,0 +1,185 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// TSF: Time Series Forecast +/// A statistical indicator that provides a linear regression forecast of future values +/// based on historical data. It includes both the forecast value and a confidence interval. +/// +/// +/// The Time Series Forecast calculation process: +/// 1. Calculates linear regression on the input data +/// 2. Extrapolates the regression line to forecast future values +/// 3. Computes confidence intervals based on the standard error of the forecast +/// +/// Key characteristics: +/// - Provides point forecast and confidence interval +/// - Based on linear regression principles +/// - Assumes trend continuity +/// - Sensitive to recent data changes +/// - Useful for short-term predictions +/// +/// Formula: +/// Forecast = a + b * (n + 1) +/// where: +/// a = y-intercept +/// b = slope +/// n = number of periods +/// +/// Confidence Interval = Forecast ± (t * SE) +/// where: +/// t = t-value for desired confidence level +/// SE = Standard Error of the forecast +/// +/// Market Applications: +/// - Price target estimation +/// - Trend analysis +/// - Risk assessment +/// - Trading strategy development +/// - Market behavior prediction +/// +/// Sources: +/// https://en.wikipedia.org/wiki/Time_series +/// "Forecasting: Principles and Practice" - Rob J Hyndman and George Athanasopoulos +/// +/// Note: Assumes linear trend in the data and may not capture non-linear patterns +/// +[SkipLocalsInit] +public sealed class Tsf : AbstractBase +{ + private readonly int Period; + private readonly CircularBuffer _values; + private const int MinimumPoints = 2; + + /// + /// The forecasted value for the next period. + /// + public double Forecast { get; private set; } + + /// + /// The lower bound of the confidence interval. + /// + public double LowerBound { get; private set; } + + /// + /// The upper bound of the confidence interval. + /// + public double UpperBound { get; private set; } + + /// The number of historical data points to consider for forecasting. + /// Thrown when period is less than 2. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Tsf(int period) + { + if (period < MinimumPoints) + { + throw new ArgumentOutOfRangeException(nameof(period), + "Period must be greater than or equal to 2 for time series forecasting."); + } + Period = period; + WarmupPeriod = MinimumPoints; + _values = new CircularBuffer(period); + Name = $"TSF(period={period})"; + Init(); + } + + /// The data source object that publishes updates. + /// The number of historical data points to consider for forecasting. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Tsf(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _values.Clear(); + Forecast = 0; + LowerBound = 0; + UpperBound = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Input.Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double slope, double intercept) CalculateLinearRegression(ReadOnlySpan values) + { + int n = values.Length; + double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0; + + for (int i = 0; i < n; i++) + { + double x = i + 1; + double y = values[i]; + sumX += x; + sumY += y; + sumXY += x * y; + sumX2 += x * x; + } + + double slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX); + double intercept = (sumY - slope * sumX) / n; + + return (slope, intercept); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateStandardError(ReadOnlySpan values, double slope, double intercept) + { + int n = values.Length; + double sumSquaredResiduals = 0; + + for (int i = 0; i < n; i++) + { + double x = i + 1; + double y = values[i]; + double predicted = slope * x + intercept; + double residual = y - predicted; + sumSquaredResiduals += residual * residual; + } + + return Math.Sqrt(sumSquaredResiduals / (n - 2)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + _values.Add(Input.Value, Input.IsNew); + + if (_values.Count >= MinimumPoints) + { + ReadOnlySpan values = _values.GetSpan(); + + var (slope, intercept) = CalculateLinearRegression(values); + + // Calculate forecast for the next period + Forecast = slope * (Period + 1) + intercept; + + // Calculate standard error + double standardError = CalculateStandardError(values, slope, intercept); + + // Calculate confidence interval (using t-distribution with n-2 degrees of freedom) + double tValue = 1.96; // Approximation for 95% confidence interval + double marginOfError = tValue * standardError * Math.Sqrt(1 + 1.0 / Period); + + LowerBound = Forecast - marginOfError; + UpperBound = Forecast + marginOfError; + } + + IsHot = _values.Count >= Period; + return Forecast; + } +} diff --git a/lib/statistics/_list.md b/lib/statistics/_list.md index 4e16b0de..3ca1af8c 100644 --- a/lib/statistics/_list.md +++ b/lib/statistics/_list.md @@ -1,22 +1,32 @@ -# Statistics indicators -Done: 13, Todo: 6 +# Statistics -*BETA - Beta coefficient (Beta, R-squared) -*CORR - Correlation Coefficient (Correlation, P-value) -✔️ CURVATURE - Rate of Change in Direction or Slope -✔️ ENTROPY - Measure of Uncertainty or Disorder -✔️ HURST - Hurst Exponent -✔️ KURTOSIS - Measure of Tails/Peakedness -✔️ MAX - Maximum with exponential decay -✔️ MEDIAN - Middle value -✔️ MIN - Minimum with exponential decay -✔️ MODE - Most Frequent Value -✔️ PERCENTILE - Rank Order -*RSQUARED - Coefficient of Determination (R-squared, Adjusted R-squared) -✔️ SKEW - Skewness, asymmetry of distribution -✔️ SLOPE - Rate of Change, Linear Regression -✔️ STDDEV - Standard Deviation, Measure of Spread -*THEIL - Theil's U Statistics (U1, U2) -*TSF - Time Series Forecast (Forecast, Confidence Interval) -✔️ VARIANCE - Average of Squared Deviations -✔️ ZSCORE - Standardized Score +Statistical functions and indicators for financial analysis. + +## Implemented + +- [Beta](Beta.cs) - Beta coefficient measuring volatility relative to market +- [Corr](Corr.cs) - Correlation coefficient between two series +- [Curvature](Curvature.cs) - Curvature of a time series +- [Entropy](Entropy.cs) - Information entropy of a series +- [Hurst](Hurst.cs) - Hurst exponent for trend strength +- [Kurtosis](Kurtosis.cs) - Kurtosis measuring tail extremity +- [Max](Max.cs) - Maximum value over period +- [Median](Median.cs) - Median value over period +- [Min](Min.cs) - Minimum value over period +- [Mode](Mode.cs) - Mode (most frequent value) +- [Percentile](Percentile.cs) - Percentile rank calculation +- [Skew](Skew.cs) - Skewness measuring distribution asymmetry +- [Slope](Slope.cs) - Linear regression slope +- [Stddev](Stddev.cs) - Standard deviation +- [Theil](Theil.cs) - Theil's U statistics for forecast accuracy +- [Tsf](Tsf.cs) - Time series forecast +- [Variance](Variance.cs) - Statistical variance +- [Zscore](Zscore.cs) - Z-score standardization + +## Planned + +- Cointegration - Test for cointegrated series +- Granger - Granger causality test +- Jarque-Bera - Normality test +- Kendall - Kendall rank correlation +- Spearman - Spearman rank correlation