diff --git a/Directory.Build.props b/Directory.Build.props index 311a99e7..a5cfd53c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -11,7 +11,7 @@ false true AnyCPU - False + True bin\$(Configuration)\ False full diff --git a/lib/averages/Afirma.cs b/lib/averages/Afirma.cs index e80f2caf..0d7f1aa2 100644 --- a/lib/averages/Afirma.cs +++ b/lib/averages/Afirma.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -31,6 +31,9 @@ public class Afirma : AbstractBase private readonly double[] _armaBuffer; private readonly int _n; private readonly double _sx2, _sx3, _sx4, _sx5, _sx6, _den; + private readonly double _twoPi = 2.0 * Math.PI; + private readonly double _fourPi = 4.0 * Math.PI; + private readonly double _sixPi = 6.0 * Math.PI; /// The number of periods for the sinc filter calculation. /// The number of filter taps (filter length). Must be odd number. @@ -56,7 +59,7 @@ public class Afirma : AbstractBase _armaBuffer = new double[taps]; _n = (Taps - 1) / 2; - // Calculate least squares coefficients in the constructor + // Precalculate least squares coefficients _sx2 = (2 * _n + 1) / 3.0; _sx3 = _n * (_n + 1) / 2.0; _sx4 = _sx2 * (3 * _n * _n + 3 * _n - 1) / 5.0; @@ -78,6 +81,7 @@ public class Afirma : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -87,6 +91,34 @@ public class Afirma : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double CalculateSincWeight(double x) + { + return Math.Abs(x) < 1e-10 ? 1.0 : Math.Sin(x) / x; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double GetWindowWeight(int k, int tapsMinusOne) + { + switch (Window) + { + case WindowType.Rectangular: + return 1.0; + case WindowType.Hanning1: + return 0.50 - 0.50 * Math.Cos(_twoPi * k / tapsMinusOne); + case WindowType.Hanning2: + return 0.54 - 0.46 * Math.Cos(_twoPi * k / tapsMinusOne); + case WindowType.Blackman: + return 0.42 - 0.50 * Math.Cos(_twoPi * k / tapsMinusOne) + 0.08 * Math.Cos(_fourPi * k / tapsMinusOne); + case WindowType.BlackmanHarris: + return 0.35875 - 0.48829 * Math.Cos(_twoPi * k / tapsMinusOne) + + 0.14128 * Math.Cos(_fourPi * k / tapsMinusOne) - + 0.01168 * Math.Cos(_sixPi * k / tapsMinusOne); + default: + return 1.0; + } + } + protected override double Calculation() { ManageState(IsNew); @@ -94,71 +126,59 @@ public class Afirma : AbstractBase if (_index >= Taps) { - double a0 = _buffer[_n]; - double a1 = _buffer[_n] - _buffer[_n + 1]; - double sx2y = 0.0; - double sx3y = 0.0; - - for (int i = 0; i <= _n; i++) - { - sx2y += i * i * _buffer[_n - i]; - sx3y += i * i * i * _buffer[_n - i]; - } - - sx2y = 2.0 * sx2y / _n / (_n + 1); - sx3y = 2.0 * sx3y / _n / (_n + 1); - double p = sx2y - a0 * _sx2 - a1 * _sx3; - double q = sx3y - a0 * _sx3 - a1 * _sx4; - double a2 = (p * _sx6 / _sx5 - q) / _den; - double a3 = (q * _sx4 / _sx5 - p) / _den; - - for (int k = 0; k <= _n; k++) - { - _armaBuffer[_n - k] = a0 + k * a1 + k * k * a2 + k * k * k * a3; - } + CalculateAdaptiveCoefficients(); } double result = 0.0; for (int k = 0; k < Taps; k++) { - result += _buffer[k] * _weights[k] / _wsum; + result += _buffer[k] * _weights[k]; } IsHot = _index >= WarmupPeriod; - return result; + return result / _wsum; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CalculateAdaptiveCoefficients() + { + double a0 = _buffer[_n]; + double a1 = _buffer[_n] - _buffer[_n + 1]; + double sx2y = 0.0; + double sx3y = 0.0; + + for (int i = 0; i <= _n; i++) + { + double i2 = i * i; + sx2y += i2 * _buffer[_n - i]; + sx3y += i2 * i * _buffer[_n - i]; + } + + sx2y = 2.0 * sx2y / _n / (_n + 1); + sx3y = 2.0 * sx3y / _n / (_n + 1); + double p = sx2y - a0 * _sx2 - a1 * _sx3; + double q = sx3y - a0 * _sx3 - a1 * _sx4; + double a2 = (p * _sx6 / _sx5 - q) / _den; + double a3 = (q * _sx4 / _sx5 - p) / _den; + + for (int k = 0; k <= _n; k++) + { + double k2 = k * k; + _armaBuffer[_n - k] = a0 + k * a1 + k2 * a2 + k2 * k * a3; + } } private double CalculateWeights() { double wsum = 0.0; double centerTap = (Taps - 1) / 2.0; + int tapsMinusOne = Taps - 1; + for (int k = 0; k < Taps; k++) { - double windowWeight; - switch (Window) - { - case WindowType.Rectangular: - windowWeight = 1.0; - break; - case WindowType.Hanning1: - windowWeight = 0.50 - 0.50 * Math.Cos(2.0 * Math.PI * k / (Taps - 1)); - break; - case WindowType.Hanning2: - windowWeight = 0.54 - 0.46 * Math.Cos(2.0 * Math.PI * k / (Taps - 1)); - break; - case WindowType.Blackman: - windowWeight = 0.42 - 0.50 * Math.Cos(2.0 * Math.PI * k / (Taps - 1)) + 0.08 * Math.Cos(4.0 * Math.PI * k / (Taps - 1)); - break; - case WindowType.BlackmanHarris: - windowWeight = 0.35875 - 0.48829 * Math.Cos(2.0 * Math.PI * k / (Taps - 1)) + 0.14128 * Math.Cos(4.0 * Math.PI * k / (Taps - 1)) - 0.01168 * Math.Cos(6.0 * Math.PI * k / (Taps - 1)); - break; - default: - windowWeight = 1.0; - break; - } - - double sincWeight; - sincWeight = Math.Abs(k - centerTap) < 1e-10 ? 1.0 : Math.Sin(Math.PI * (k - centerTap) / Periods) / (Math.PI * (k - centerTap) / Periods); + double windowWeight = GetWindowWeight(k, tapsMinusOne); + double x = Math.PI * (k - centerTap) / Periods; + double sincWeight = CalculateSincWeight(x); _weights[k] = windowWeight * sincWeight; wsum += _weights[k]; diff --git a/lib/averages/Convolution.cs b/lib/averages/Convolution.cs index 4a7a75fc..08162bc8 100644 --- a/lib/averages/Convolution.cs +++ b/lib/averages/Convolution.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -17,6 +17,7 @@ public class Convolution : AbstractBase private readonly int _kernelSize; private readonly CircularBuffer _buffer; private readonly double[] _normalizedKernel; + private int _activeLength; /// Array of weights defining the convolution operation. The length of this array determines the filter's window size. /// Thrown when kernel is null or empty. @@ -41,22 +42,27 @@ public class Convolution : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private new void Init() { base.Init(); _buffer.Clear(); - Array.Copy(_kernel, _normalizedKernel, _kernelSize); + System.Array.Copy(_kernel, _normalizedKernel, _kernelSize); + _activeLength = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) { _lastValidValue = Input.Value; _index++; + _activeLength = System.Math.Min(_index, _kernelSize); } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override double GetLastValid() { return _lastValidValue; @@ -65,7 +71,6 @@ public class Convolution : AbstractBase protected override double Calculation() { ManageState(Input.IsNew); - _buffer.Add(Input.Value, Input.IsNew); // Normalize kernel on each calculation until buffer is full @@ -80,37 +85,56 @@ public class Convolution : AbstractBase return result; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void NormalizeKernel() { - int activeLength = Math.Min(_index, _kernelSize); double sum = 0; // Calculate the sum of the active kernel elements - for (int i = 0; i < activeLength; i++) + for (int i = 0; i < _activeLength; i++) { sum += _kernel[i]; } // Normalize the kernel or set equal weights if the sum is zero - double normalizationFactor = (sum != 0) ? sum : activeLength; - for (int i = 0; i < activeLength; i++) + double normalizationFactor = (sum != 0) ? sum : _activeLength; + double invNormFactor = 1.0 / normalizationFactor; + + for (int i = 0; i < _activeLength; i++) { - _normalizedKernel[i] = _kernel[i] / normalizationFactor; + _normalizedKernel[i] = _kernel[i] * invNormFactor; } // Set the rest of the normalized kernel to zero - Array.Clear(_normalizedKernel, activeLength, _kernelSize - activeLength); + if (_activeLength < _kernelSize) + { + System.Array.Clear(_normalizedKernel, _activeLength, _kernelSize - _activeLength); + } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private double ConvolveBuffer() { double sum = 0; var bufferSpan = _buffer.GetSpan(); - int activeLength = Math.Min(_index, _kernelSize); + int offset = _activeLength - 1; - for (int i = 0; i < activeLength; i++) + // Unroll the loop for better performance when possible + int i = 0; + while (i <= offset - 3) { - sum += bufferSpan[activeLength - 1 - i] * _normalizedKernel[i]; + sum += bufferSpan[offset - i] * _normalizedKernel[i] + + bufferSpan[offset - (i + 1)] * _normalizedKernel[i + 1] + + bufferSpan[offset - (i + 2)] * _normalizedKernel[i + 2] + + bufferSpan[offset - (i + 3)] * _normalizedKernel[i + 3]; + i += 4; + } + + // Handle remaining elements + while (i < _activeLength) + { + sum += bufferSpan[offset - i] * _normalizedKernel[i]; + i++; } return sum; diff --git a/lib/averages/Dema.cs b/lib/averages/Dema.cs index d3e37bfb..d0673ae4 100644 --- a/lib/averages/Dema.cs +++ b/lib/averages/Dema.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -7,12 +8,8 @@ namespace QuanTAlib; /// smoothness, at the cost of overshooting the signal line. /// /// -/// Smoothness: ★★★☆☆ (3/5) -/// Sensitivity: ★★★★☆ (4/5) -/// Overshooting: ★★★☆☆ (3/5) -/// Lag: ★★★★☆ (4/5) -/// /// Sources: +/// https://en.wikipedia.org/wiki/Double_exponential_moving_average /// https://www.investopedia.com/terms/d/double-exponential-moving-average.asp /// https://www.tradingview.com/support/solutions/43000502589-double-exponential-moving-average-dema/ /// @@ -21,12 +18,12 @@ namespace QuanTAlib; /// public class Dema : AbstractBase { - // inherited _index - // inherited _value private readonly int _period; + private readonly double _k; + private readonly double _epsilon = 1e-10; private double _lastEma1, _p_lastEma1; private double _lastEma2, _p_lastEma2; - private double _k, _e, _p_e; + private double _e, _p_e; public Dema(int period) { @@ -35,9 +32,10 @@ public class Dema : AbstractBase throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); } _period = period; + _k = 2.0 / (_period + 1); Name = "Dema"; double percentile = 0.85; //targeting 85th percentile of correctness of converging EMA - WarmupPeriod = (int)Math.Ceiling(-period * Math.Log(1 - percentile)); + WarmupPeriod = (int)System.Math.Ceiling(-period * System.Math.Log(1 - percentile)); Init(); } @@ -46,17 +44,17 @@ public class Dema : AbstractBase var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - //inhereted public void Sub(object source, in ValueEventArgs args) + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); - _k = 2.0 / (_period + 1); _e = 1.0; _lastEma1 = 0; _lastEma2 = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -74,30 +72,31 @@ public class Dema : AbstractBase } } - /// - /// Core DEMA calculation - /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateEma(double input, double lastEma) + { + return _k * (input - lastEma) + lastEma; + } + protected override double Calculation() { ManageState(Input.IsNew); - double result, _ema1, _ema2; - // compensator for early ema values - _e = (_e > 1e-10) ? (1 - _k) * _e : 0; - double _invE = (_e > 1e-10) ? 1 / (1 - _e) : 1; + // Compensator for early EMA values + _e = (_e > _epsilon) ? (1 - _k) * _e : 0; + double invE = (_e > _epsilon) ? 1 / (1 - _e) : 1; - // Calculate EMA1 - _ema1 = _k * (Input.Value - _lastEma1) + _lastEma1; + // Calculate EMAs + double ema1 = CalculateEma(Input.Value, _lastEma1); + double compensatedEma1 = ema1 * invE; + double ema2 = CalculateEma(compensatedEma1, _lastEma2); - // Calculate EMA2 using compensatedEma1 - _ema2 = _k * (_ema1 * _invE - _lastEma2) + _lastEma2; + // Store values for next iteration + _lastEma1 = ema1; + _lastEma2 = ema2; - // Calculate DEMA - double _dema = 2 * _ema1 * _invE - (_ema2 * _invE); - - result = _dema; - _lastEma1 = _ema1; - _lastEma2 = _ema2; + // Calculate final DEMA + double result = 2 * compensatedEma1 - (ema2 * invE); IsHot = _index >= WarmupPeriod; return result; diff --git a/lib/averages/Dsma.cs b/lib/averages/Dsma.cs index 5fb0c9ba..70f80191 100644 --- a/lib/averages/Dsma.cs +++ b/lib/averages/Dsma.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -25,6 +26,10 @@ public class Dsma : AbstractBase private readonly CircularBuffer _buffer; private readonly double _c1, _c2, _c3; private readonly double _scaleFactor; + private readonly double _periodRecip; // 1/_period + private readonly double _scaleByPeriod; // 5/_period + private readonly double _c1Half; // _c1/2 + private double _lastDsma, _p_lastDsma; private double _filt, _filt1, _filt2, _zeros, _zeros1; private double _p_filt, _p_filt1, _p_filt2, _p_zeros, _p_zeros1; @@ -46,16 +51,20 @@ public class Dsma : AbstractBase throw new ArgumentOutOfRangeException(nameof(scaleFactor), "Scale factor must be between 0 and 1 (exclusive)."); } _period = period; + _periodRecip = 1.0 / period; _scaleFactor = scaleFactor; _buffer = new CircularBuffer(period); // SuperSmoother filter coefficients - double _a1 = Math.Exp(-1.414 * Math.PI / (0.5 * period)); - double _b1 = 2 * _a1 * Math.Cos(1.414 * Math.PI / (0.5 * period)); + double halfPeriod = 0.5 * period; + double a1 = System.Math.Exp(-1.414 * System.Math.PI / halfPeriod); + double b1 = 2.0 * a1 * System.Math.Cos(1.414 * System.Math.PI / halfPeriod); - _c2 = _b1; - _c3 = -_a1 * _a1; - _c1 = 1 - _c2 - _c3; + _c2 = b1; + _c3 = -a1 * a1; + _c1 = 1.0 - _c2 - _c3; + _c1Half = _c1 * 0.5; + _scaleByPeriod = 5.0 / period; Name = "Dsma"; WarmupPeriod = (int)(period * 1.5); // A conservative estimate @@ -68,6 +77,7 @@ public class Dsma : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -77,6 +87,7 @@ public class Dsma : AbstractBase _isInit = false; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -102,6 +113,19 @@ public class Dsma : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateSuperSmootherFilter() + { + return _c1Half * (_zeros + _zeros1) + _c2 * _filt1 + _c3 * _filt2; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateAdaptiveAlpha(double scaledFilt) + { + double alpha = _scaleFactor * System.Math.Abs(scaledFilt) * _scaleByPeriod; + return System.Math.Clamp(alpha, 0.1, 1.0); + } + protected override double Calculation() { ManageState(Input.IsNew); @@ -117,20 +141,18 @@ public class Dsma : AbstractBase _zeros = Input.Value - _lastDsma; // SuperSmoother Filter - _filt = _c1 * (_zeros + _zeros1) / 2 + _c2 * _filt1 + _c3 * _filt2; + _filt = CalculateSuperSmootherFilter(); // Update buffer for RMS calculation - _buffer.Add(_filt * _filt, Input.IsNew); + double filtSquared = _filt * _filt; + _buffer.Add(filtSquared, Input.IsNew); // Compute RMS (Root Mean Square) - double rms = Math.Sqrt(_buffer.Sum() / _period); + double rms = System.Math.Sqrt(_buffer.Sum() * _periodRecip); - // Rescale Filt in terms of Standard Deviations - double scaledFilt = rms != 0 ? _filt / rms : 0; - - // Calculate adaptive alpha - double alpha = _scaleFactor * Math.Abs(scaledFilt) * 5 / _period; - alpha = Math.Max(0.1, Math.Min(1.0, alpha)); + // Rescale Filt in terms of Standard Deviations and calculate adaptive alpha + double scaledFilt = rms > 0 ? _filt / rms : 0; + double alpha = CalculateAdaptiveAlpha(scaledFilt); // DSMA calculation double dsma = alpha * Input.Value + (1 - alpha) * _lastDsma; diff --git a/lib/averages/Dwma.cs b/lib/averages/Dwma.cs index ba09d84c..88c87c7b 100644 --- a/lib/averages/Dwma.cs +++ b/lib/averages/Dwma.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -25,16 +26,18 @@ public class Dwma : AbstractBase { private readonly Wma _innerWma; private readonly Wma _outerWma; + private readonly int _period; public Dwma(int period) { if (period < 1) { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } + _period = period; _innerWma = new Wma(period); _outerWma = new Wma(period); - Name = "Wma"; + Name = "Dwma"; WarmupPeriod = 2 * period - 1; Init(); } @@ -45,6 +48,7 @@ public class Dwma : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -52,6 +56,7 @@ public class Dwma : AbstractBase _outerWma.Init(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -61,19 +66,23 @@ public class Dwma : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override double GetLastValid() + { + return _lastValidValue; + } + protected override double Calculation() { ManageState(Input.IsNew); // Calculate inner WMA - TValue innerResult = _innerWma.Calc(Input); + var innerResult = _innerWma.Calc(Input); // Calculate outer WMA using the result of inner WMA - TValue outerResult = _outerWma.Calc(innerResult); + var outerResult = _outerWma.Calc(innerResult); - double result = outerResult.Value; IsHot = _index >= WarmupPeriod; - - return result; + return outerResult.Value; } } diff --git a/lib/averages/Ema.cs b/lib/averages/Ema.cs index 3e38f745..7ebd8014 100644 --- a/lib/averages/Ema.cs +++ b/lib/averages/Ema.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -22,43 +23,14 @@ namespace QuanTAlib; /// public class Ema : AbstractBase { - // inherited _index - // inherited _value - - /// - /// The period for the EMA calculation. - /// private readonly int _period; - - /// - /// Circular buffer for SMA calculation. - /// - private CircularBuffer _sma; - - /// - /// The last calculated EMA value. - /// - private double _lastEma, _p_lastEma; - - /// - /// Compensator for early EMA values. - /// - private double _e, _p_e; - - /// - /// The smoothing factor for EMA calculation. - /// private readonly double _k; - - /// - /// Flags to track initialization status. - /// - private bool _isInit, _p_isInit; - - /// - /// Flag to determine whether to use SMA for initial values. - /// private readonly bool _useSma; + private readonly double _epsilon = 1e-10; + private CircularBuffer _sma; + private double _lastEma, _p_lastEma; + private double _e, _p_e; + private bool _isInit, _p_isInit; /// /// Initializes a new instance of the Ema class with a specified period. @@ -70,14 +42,14 @@ public class Ema : AbstractBase { if (period < 1) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); + throw new System.ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); } _period = period; _k = 2.0 / (_period + 1); _useSma = useSma; - _sma = new(period); + _sma = new(_period); Name = "Ema"; - WarmupPeriod = (int)Math.Ceiling(Math.Log(0.05) / Math.Log(1 - _k)); //95th percentile + WarmupPeriod = (int)System.Math.Ceiling(System.Math.Log(0.05) / System.Math.Log(1 - _k)); //95th percentile Init(); } @@ -92,7 +64,7 @@ public class Ema : AbstractBase _sma = new(1); Name = "Ema"; _period = 1; - WarmupPeriod = (int)Math.Ceiling(Math.Log(0.05) / Math.Log(1 - _k)); //95th percentile + WarmupPeriod = (int)System.Math.Ceiling(System.Math.Log(0.05) / System.Math.Log(1 - _k)); //95th percentile Init(); } @@ -108,9 +80,7 @@ public class Ema : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - /// - /// Initializes the Ema instance. - /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -121,10 +91,7 @@ public class Ema : AbstractBase _sma = new(_period); } - /// - /// Manages the state of the Ema instance. - /// - /// Indicates whether the input is new. + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -142,21 +109,27 @@ public class Ema : AbstractBase } } - /// - /// Performs the EMA calculation. - /// - /// The calculated EMA value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateEma(double input, double lastEma) + { + return _k * (input - lastEma) + lastEma; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CompensateEma(double ema) + { + return (_useSma || _e <= _epsilon) ? ema : ema / (1 - _e); + } + protected override double Calculation() { - double result, _ema; ManageState(Input.IsNew); - // when _UseSma == true, use SMA calculation until we have enough data points + double ema; if (!_isInit && _useSma) { _sma.Add(Input.Value, Input.IsNew); - _ema = _sma.Average(); - result = _ema; + ema = _sma.Average(); if (_index >= _period) { _isInit = true; @@ -164,16 +137,14 @@ public class Ema : AbstractBase } else { - // compensator for early ema values - _e = (_e > 1e-10) ? (1 - _k) * _e : 0; - - _ema = _k * (Input.Value - _lastEma) + _lastEma; - - // _useSma decides if we use compensator or not - result = (_useSma || _e <= double.Epsilon) ? _ema : _ema / (1 - _e); + // Compensator for early EMA values + _e = (_e > _epsilon) ? (1 - _k) * _e : 0; + ema = CalculateEma(Input.Value, _lastEma); + ema = CompensateEma(ema); } - _lastEma = _ema; + + _lastEma = ema; IsHot = _index >= WarmupPeriod; - return result; + return ema; } } diff --git a/lib/averages/Epma.cs b/lib/averages/Epma.cs index f9985e9b..170a0319 100644 --- a/lib/averages/Epma.cs +++ b/lib/averages/Epma.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -27,6 +27,7 @@ public class Epma : AbstractBase { private readonly int _period; private readonly Convolution _convolution; + private readonly double[] _baseKernel; /// The number of data points used in the EPMA calculation. /// Thrown when period is less than 1. @@ -34,10 +35,11 @@ public class Epma : AbstractBase { if (period < 1) { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } _period = period; - _convolution = new Convolution(GenerateKernel(_period)); + _baseKernel = GenerateKernel(_period); + _convolution = new Convolution(_baseKernel); Name = "Epma"; WarmupPeriod = period; Init(); @@ -51,12 +53,14 @@ public class Epma : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private new void Init() { base.Init(); _convolution.Init(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -66,24 +70,31 @@ public class Epma : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double CalculateKernelSum(int period) + { + // Using arithmetic sequence sum formula: n(a1 + an)/2 + // where a1 = (2p-1) and an = (2p-1) - 3(n-1) + double firstTerm = 2 * period - 1; + double lastTerm = firstTerm - 3 * (period - 1); + return period * (firstTerm + lastTerm) * 0.5; + } + protected override double Calculation() { ManageState(Input.IsNew); // Use Convolution for calculation - TValue convolutionResult = _convolution.Calc(Input); - + var convolutionResult = _convolution.Calc(Input); double result = convolutionResult.Value; // Adjust for partial periods during warmup if (_index < _period) { - double[] partialKernel = GenerateKernel(_index); - result /= partialKernel.Sum(); + result *= CalculateKernelSum(_period) / CalculateKernelSum(_index); } IsHot = _index >= WarmupPeriod; - return result; } @@ -92,21 +103,17 @@ public class Epma : AbstractBase /// /// The period for which to generate the kernel. /// An array of normalized weights for the convolution operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double[] GenerateKernel(int period) { double[] kernel = new double[period]; - double weightSum = 0; + double weightSum = CalculateKernelSum(period); + double invWeightSum = 1.0 / weightSum; + double baseWeight = 2 * period - 1; for (int i = 0; i < period; i++) { - kernel[i] = (2 * period - 1) - 3 * i; - weightSum += kernel[i]; - } - - // Normalize the kernel - for (int i = 0; i < period; i++) - { - kernel[i] /= weightSum; + kernel[i] = (baseWeight - 3 * i) * invWeightSum; } return kernel; diff --git a/lib/averages/Frama.cs b/lib/averages/Frama.cs index 9e509b82..aa4adaca 100644 --- a/lib/averages/Frama.cs +++ b/lib/averages/Frama.cs @@ -1,5 +1,4 @@ -using System; - +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -28,6 +27,11 @@ namespace QuanTAlib; public class Frama : AbstractBase { private readonly int _period; + private readonly int _halfPeriod; + private readonly double _periodRecip; + private readonly double _halfPeriodRecip; + private readonly double _log2 = System.Math.Log(2); + private readonly double _epsilon = double.Epsilon; private readonly CircularBuffer _buffer; private double _lastFrama; private double _prevLastFrama; @@ -37,9 +41,12 @@ public class Frama : AbstractBase public Frama(int period) { if (period < 2) - throw new ArgumentException("Period must be at least 2", nameof(period)); + throw new System.ArgumentException("Period must be at least 2", nameof(period)); _period = period; + _halfPeriod = period / 2; + _periodRecip = 1.0 / period; + _halfPeriodRecip = 1.0 / _halfPeriod; _buffer = new CircularBuffer(period); WarmupPeriod = period; } @@ -52,6 +59,7 @@ public class Frama : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -60,6 +68,7 @@ public class Frama : AbstractBase _prevLastFrama = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -73,6 +82,26 @@ public class Frama : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void UpdateMinMax(double price, ref double high, ref double low) + { + high = System.Math.Max(high, price); + low = System.Math.Min(low, price); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateAlpha(double dimension) + { + double alpha = System.Math.Exp(-4.6 * (dimension - 1)); + return System.Math.Clamp(alpha, 0.01, 1.0); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override double GetLastValid() + { + return _lastFrama; + } + protected override double Calculation() { ManageState(Input.IsNew); @@ -85,7 +114,6 @@ public class Frama : AbstractBase return _lastFrama; } - int half = _period / 2; double hh = double.MinValue, ll = double.MaxValue; double hh1 = double.MinValue, ll1 = double.MaxValue; double hh2 = double.MinValue, ll2 = double.MaxValue; @@ -93,37 +121,27 @@ public class Frama : AbstractBase for (int i = 0; i < _period; i++) { double price = _buffer[i]; - hh = Math.Max(hh, price); - ll = Math.Min(ll, price); + UpdateMinMax(price, ref hh, ref ll); - if (i < half) + if (i < _halfPeriod) { - hh1 = Math.Max(hh1, price); - ll1 = Math.Min(ll1, price); + UpdateMinMax(price, ref hh1, ref ll1); } else { - hh2 = Math.Max(hh2, price); - ll2 = Math.Min(ll2, price); + UpdateMinMax(price, ref hh2, ref ll2); } } - double n1 = (hh - ll) / _period; - double n2 = (hh1 - ll1 + hh2 - ll2) / (_period / 2); + double n1 = (hh - ll) * _periodRecip; + double n2 = (hh1 - ll1 + hh2 - ll2) * _halfPeriodRecip; - double d = (Math.Log(n2 + double.Epsilon) - Math.Log(n1 + double.Epsilon)) / Math.Log(2); - - double alpha = Math.Exp(-4.6 * (d - 1)); - alpha = Math.Max(Math.Min(alpha, 1), 0.01); // Ensure alpha is between 0.01 and 1 + double dimension = (System.Math.Log(n2 + _epsilon) - System.Math.Log(n1 + _epsilon)) / _log2; + double alpha = CalculateAlpha(dimension); _lastFrama = alpha * (Input.Value - _lastFrama) + _lastFrama; IsHot = _index >= WarmupPeriod; return _lastFrama; } - - protected override double GetLastValid() - { - return _lastFrama; - } } diff --git a/lib/averages/Fwma.cs b/lib/averages/Fwma.cs index 98ee1bb1..bff1bf4d 100644 --- a/lib/averages/Fwma.cs +++ b/lib/averages/Fwma.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -27,6 +27,7 @@ namespace QuanTAlib; public class Fwma : AbstractBase { private readonly Convolution _convolution; + private readonly double[] _kernel; /// The number of data points used in the FWMA calculation. /// Thrown when period is less than 1. @@ -34,9 +35,10 @@ public class Fwma : AbstractBase { if (period < 1) { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } - _convolution = new Convolution(GenerateKernel(period)); + _kernel = GenerateKernel(period); + _convolution = new Convolution(_kernel); Name = "Fwma"; WarmupPeriod = period; Init(); @@ -55,41 +57,42 @@ public class Fwma : AbstractBase /// /// The period for which to generate the kernel. /// An array of normalized Fibonacci-based weights for the convolution operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double[] GenerateKernel(int period) { double[] kernel = new double[period]; double[] fibSeries = new double[period]; - double weightSum = 0; - // Generate Fibonacci series + // Generate Fibonacci series with running sum fibSeries[0] = fibSeries[1] = 1; + double weightSum = 2.0; // Initial sum for first two Fibonacci numbers + for (int i = 2; i < period; i++) { fibSeries[i] = fibSeries[i - 1] + fibSeries[i - 2]; + weightSum += fibSeries[i]; } - // Reverse the series to give more weight to recent prices - for (int i = 0; i < period; i++) - { - kernel[i] = fibSeries[period - 1 - i]; - weightSum += kernel[i]; - } + // Calculate inverse of weight sum for normalization + double invWeightSum = 1.0 / weightSum; - // Normalize the kernel + // Reverse and normalize the series in one pass for (int i = 0; i < period; i++) { - kernel[i] /= weightSum; + kernel[i] = fibSeries[period - 1 - i] * invWeightSum; } return kernel; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private new void Init() { base.Init(); _convolution.Init(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -104,11 +107,9 @@ public class Fwma : AbstractBase ManageState(Input.IsNew); // Use Convolution for calculation - TValue convolutionResult = _convolution.Calc(Input); - - double result = convolutionResult.Value; + var convolutionResult = _convolution.Calc(Input); IsHot = _index >= WarmupPeriod; - return result; + return convolutionResult.Value; } } diff --git a/lib/averages/Gma.cs b/lib/averages/Gma.cs index 74fafc08..71d80a88 100644 --- a/lib/averages/Gma.cs +++ b/lib/averages/Gma.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -27,6 +27,7 @@ namespace QuanTAlib; public class Gma : AbstractBase { private readonly Convolution _convolution; + private readonly double[] _kernel; /// The number of data points used in the GMA calculation. /// Thrown when period is less than 1. @@ -34,9 +35,10 @@ public class Gma : AbstractBase { if (period < 1) { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } - _convolution = new Convolution(GenerateKernel(period)); + _kernel = GenerateKernel(period); + _convolution = new Convolution(_kernel); Name = "Gma"; WarmupPeriod = period; Init(); @@ -56,34 +58,41 @@ public class Gma : AbstractBase /// The period for which to generate the kernel. /// The standard deviation parameter controlling the spread of the Gaussian curve. Default is 1.0. /// An array of normalized Gaussian-based weights for the convolution operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double[] GenerateKernel(int period, double sigma = 1.0) { double[] kernel = new double[period]; double weightSum = 0; int center = period / 2; + double centerRecip = 1.0 / center; + double sigmaSquared2 = 2.0 * sigma * sigma; + // Calculate weights and sum in one pass for (int i = 0; i < period; i++) { - double x = (i - center) / (double)center; - kernel[i] = Math.Exp(-(x * x) / (2 * sigma * sigma)); + double x = (i - center) * centerRecip; + kernel[i] = System.Math.Exp(-(x * x) / sigmaSquared2); weightSum += kernel[i]; } - // Normalize the kernel + // Normalize using multiplication instead of division + double invWeightSum = 1.0 / weightSum; for (int i = 0; i < period; i++) { - kernel[i] /= weightSum; + kernel[i] *= invWeightSum; } return kernel; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private new void Init() { base.Init(); _convolution.Init(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -98,11 +107,9 @@ public class Gma : AbstractBase ManageState(Input.IsNew); // Use Convolution for calculation - TValue convolutionResult = _convolution.Calc(Input); - - double result = convolutionResult.Value; + var convolutionResult = _convolution.Calc(Input); IsHot = _index >= WarmupPeriod; - return result; + return convolutionResult.Value; } } diff --git a/lib/averages/Hma.cs b/lib/averages/Hma.cs index 5b78c796..20e4b833 100644 --- a/lib/averages/Hma.cs +++ b/lib/averages/Hma.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -29,6 +29,11 @@ namespace QuanTAlib; public class Hma : AbstractBase { private readonly Convolution _wmaHalf, _wmaFull, _wmaFinal; + private readonly int _period; + private readonly int _sqrtPeriod; + private readonly double[] _kernelHalf; + private readonly double[] _kernelFull; + private readonly double[] _kernelFinal; /// The number of data points used in the HMA calculation. Must be at least 2. /// Thrown when period is less than 2. @@ -36,12 +41,21 @@ public class Hma : AbstractBase { if (period < 2) { - throw new ArgumentException("Period must be greater than or equal to 2.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 2.", nameof(period)); } - int _sqrtPeriod = (int)Math.Sqrt(period); - _wmaHalf = new Convolution(GenerateWmaKernel(period / 2)); - _wmaFull = new Convolution(GenerateWmaKernel(period)); - _wmaFinal = new Convolution(GenerateWmaKernel(_sqrtPeriod)); + _period = period; + _sqrtPeriod = (int)System.Math.Sqrt(period); + + // Generate all kernels once + _kernelHalf = GenerateWmaKernel(period / 2); + _kernelFull = GenerateWmaKernel(period); + _kernelFinal = GenerateWmaKernel(_sqrtPeriod); + + // Initialize convolutions with pre-generated kernels + _wmaHalf = new Convolution(_kernelHalf); + _wmaFull = new Convolution(_kernelFull); + _wmaFinal = new Convolution(_kernelFinal); + Name = "Hma"; WarmupPeriod = period + _sqrtPeriod - 1; Init(); @@ -60,19 +74,22 @@ public class Hma : AbstractBase /// /// The period for which to generate the kernel. /// An array of linearly weighted values for the convolution operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double[] GenerateWmaKernel(int period) { double[] kernel = new double[period]; - double weightSum = period * (period + 1) / 2.0; + double weightSum = period * (period + 1) * 0.5; // Multiply by 0.5 instead of dividing by 2 + double invWeightSum = 1.0 / weightSum; for (int i = 0; i < period; i++) { - kernel[i] = (period - i) / weightSum; + kernel[i] = (period - i) * invWeightSum; } return kernel; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private new void Init() { base.Init(); @@ -81,6 +98,7 @@ public class Hma : AbstractBase _wmaFinal.Init(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -99,10 +117,11 @@ public class Hma : AbstractBase double wmaFullResult = _wmaFull.Calc(Input).Value; // Calculate 2*WMA(n/2) - WMA(n) - double intermediateResult = 2 * wmaHalfResult - wmaFullResult; + double intermediateResult = 2.0 * wmaHalfResult - wmaFullResult; // Calculate final WMA - double result = _wmaFinal.Calc(new TValue(Input.Time, intermediateResult, Input.IsNew)).Value; + var finalInput = new TValue(Input.Time, intermediateResult, Input.IsNew); + double result = _wmaFinal.Calc(finalInput).Value; IsHot = _index >= WarmupPeriod; return result; diff --git a/lib/averages/Htit.cs b/lib/averages/Htit.cs index 0c911f88..ea6dd6eb 100644 --- a/lib/averages/Htit.cs +++ b/lib/averages/Htit.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -42,28 +42,30 @@ public class Htit : AbstractBase private readonly CircularBuffer _sdBuffer = new(2); private readonly CircularBuffer _itBuffer = new(4); + private const double ALPHA = 0.2; + private const double BETA = 0.8; + private const double TWO_PI = 2.0 * System.Math.PI; + private const double MIN_PERIOD = 6.0; + private const double MAX_PERIOD = 50.0; + private const double PERIOD_UPPER_LIMIT = 1.5; + private const double PERIOD_LOWER_LIMIT = 0.67; + private double _lastPd = 0; private double _p_lastPd = 0; - /// - /// Initializes a new instance of the Htit class. - /// public Htit() { Name = "Htit"; WarmupPeriod = 12; } - /// - /// Initializes a new instance of the Htit class with a specified source. - /// - /// The data source object that publishes updates. public Htit(object source) : this() { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -77,6 +79,26 @@ public class Htit : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double CalculateSmoothedPrice(double p0, double p1, double p2, double p3) + { + return (4.0 * p0 + 3.0 * p1 + 2.0 * p2 + p3) * 0.1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double CalculateHilbertTransform(double b0, double b2, double b4, double b6, double adj) + { + return (0.0962 * (b0 - b6) + 0.5769 * (b2 - b4)) * adj; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double ClampPeriod(double pd, double lastPd) + { + pd = pd > PERIOD_UPPER_LIMIT * lastPd ? PERIOD_UPPER_LIMIT * lastPd : pd; + pd = pd < PERIOD_LOWER_LIMIT * lastPd ? PERIOD_LOWER_LIMIT * lastPd : pd; + return System.Math.Clamp(pd, MIN_PERIOD, MAX_PERIOD); + } + protected override double Calculation() { ManageState(Input.IsNew); @@ -100,65 +122,52 @@ public class Htit : AbstractBase return pr; } - double adj = (0.075 * _lastPd) + 0.54; + double adj = 0.075 * _lastPd + 0.54; // Smooth and detrender - double sp = ((4 * _priceBuffer[0]) + (3 * _priceBuffer[1]) + (2 * _priceBuffer[2]) + _priceBuffer[3]) / 10; + double sp = CalculateSmoothedPrice(_priceBuffer[0], _priceBuffer[1], _priceBuffer[2], _priceBuffer[3]); _spBuffer.Add(sp, Input.IsNew); - double dt = ((0.0962 * _spBuffer[0]) + (0.5769 * _spBuffer[2]) - (0.5769 * _spBuffer[4]) - (0.0962 * _spBuffer[6])) * adj; + double dt = CalculateHilbertTransform(_spBuffer[0], _spBuffer[2], _spBuffer[4], _spBuffer[6], adj); _dtBuffer.Add(dt, Input.IsNew); // In-phase and quadrature - double q1 = ((0.0962 * _dtBuffer[0]) + (0.5769 * _dtBuffer[2]) - (0.5769 * _dtBuffer[4]) - (0.0962 * _dtBuffer[6])) * adj; + double q1 = CalculateHilbertTransform(_dtBuffer[0], _dtBuffer[2], _dtBuffer[4], _dtBuffer[6], adj); _q1Buffer.Add(q1, Input.IsNew); double i1 = _dtBuffer[3]; _i1Buffer.Add(i1, Input.IsNew); // Advance the phases by 90 degrees - double jI = ((0.0962 * _i1Buffer[0]) + (0.5769 * _i1Buffer[2]) - (0.5769 * _i1Buffer[4]) - (0.0962 * _i1Buffer[6])) * adj; - double jQ = ((0.0962 * _q1Buffer[0]) + (0.5769 * _q1Buffer[2]) - (0.5769 * _q1Buffer[4]) - (0.0962 * _q1Buffer[6])) * adj; + double jI = CalculateHilbertTransform(_i1Buffer[0], _i1Buffer[2], _i1Buffer[4], _i1Buffer[6], adj); + double jQ = CalculateHilbertTransform(_q1Buffer[0], _q1Buffer[2], _q1Buffer[4], _q1Buffer[6], adj); // Phasor addition for 3-bar averaging - double i2 = i1 - jQ; - double q2 = q1 + jI; - - i2 = (0.2 * i2) + (0.8 * _i2Buffer[0]); - q2 = (0.2 * q2) + (0.8 * _q2Buffer[0]); + double i2 = ALPHA * (i1 - jQ) + BETA * _i2Buffer[0]; + double q2 = ALPHA * (q1 + jI) + BETA * _q2Buffer[0]; _i2Buffer.Add(i2, Input.IsNew); _q2Buffer.Add(q2, Input.IsNew); // Homodyne discriminator - double re = (i2 * _i2Buffer[1]) + (q2 * _q2Buffer[1]); - double im = (i2 * _q2Buffer[1]) - (q2 * _i2Buffer[1]); - - re = (0.2 * re) + (0.8 * _reBuffer[0]); - im = (0.2 * im) + (0.8 * _imBuffer[0]); + double re = ALPHA * (i2 * _i2Buffer[1] + q2 * _q2Buffer[1]) + BETA * _reBuffer[0]; + double im = ALPHA * (i2 * _q2Buffer[1] - q2 * _i2Buffer[1]) + BETA * _imBuffer[0]; _reBuffer.Add(re, Input.IsNew); _imBuffer.Add(im, Input.IsNew); // Calculate period - double pd = (im != 0 && re != 0) ? 2 * Math.PI / Math.Atan(im / re) : 0; - - // Adjust period to thresholds - pd = (pd > 1.5 * _lastPd) ? 1.5 * _lastPd : pd; - pd = (pd < 0.67 * _lastPd) ? 0.67 * _lastPd : pd; - pd = (pd < 6) ? 6 : pd; - pd = (pd > 50) ? 50 : pd; - - // Smooth the period - pd = (0.2 * pd) + (0.8 * _lastPd); + double pd = (im != 0 && re != 0) ? TWO_PI / System.Math.Atan(im / re) : 0; + pd = ClampPeriod(pd, _lastPd); + pd = ALPHA * pd + BETA * _lastPd; _pdBuffer.Add(pd, Input.IsNew); - double sd = (0.33 * pd) + (0.67 * _sdBuffer[0]); + double sd = 0.33 * pd + 0.67 * _sdBuffer[0]; _sdBuffer.Add(sd, Input.IsNew); // Smooth dominant cycle period int dcPeriods = (int)(sd + 0.5); - double sumPr = _priceBuffer.GetSpan().Slice(0, Math.Min(dcPeriods, _priceBuffer.Count)).ToArray().Sum(); + double sumPr = _priceBuffer.GetSpan().Slice(0, System.Math.Min(dcPeriods, _priceBuffer.Count)).ToArray().Sum(); double it = dcPeriods > 0 ? sumPr / dcPeriods : pr; _itBuffer.Add(it, Input.IsNew); @@ -166,9 +175,9 @@ public class Htit : AbstractBase _lastPd = pd; // Final indicator - if (_index >= 11) // 12th bar + if (_index >= 11) { - return ((4 * _itBuffer[0]) + (3 * _itBuffer[1]) + (2 * _itBuffer[2]) + _itBuffer[3]) / 10; + return CalculateSmoothedPrice(_itBuffer[0], _itBuffer[1], _itBuffer[2], _itBuffer[3]); } return pr; diff --git a/lib/averages/Hwma.cs b/lib/averages/Hwma.cs index 20c67659..8927b8eb 100644 --- a/lib/averages/Hwma.cs +++ b/lib/averages/Hwma.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -32,6 +32,8 @@ public class Hwma : AbstractBase { private readonly int _period; private readonly double _nA, _nB, _nC; + private readonly double _oneMinusNa, _oneMinusNb, _oneMinusNc; + private readonly double _halfA = 0.5; private double _pF, _pV, _pA; private double _ppF, _ppV, _ppA; @@ -56,12 +58,15 @@ public class Hwma : AbstractBase { if (period < 1) { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } _period = period; _nA = nA; _nB = nB; _nC = nC; + _oneMinusNa = 1.0 - nA; + _oneMinusNb = 1.0 - nB; + _oneMinusNc = 1.0 - nC; WarmupPeriod = period; Name = $"Hwma({_period})"; Init(); @@ -75,6 +80,7 @@ public class Hwma : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -82,6 +88,7 @@ public class Hwma : AbstractBase _ppF = _ppV = _ppA = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -100,6 +107,24 @@ public class Hwma : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateLevel(double input) + { + return _oneMinusNa * (_pF + _pV + _halfA * _pA) + _nA * input; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateVelocity(double F) + { + return _oneMinusNb * (_pV + _pA) + _nB * (F - _pF); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateAcceleration(double V) + { + return _oneMinusNc * _pA + _nC * (V - _pV); + } + protected override double Calculation() { ManageState(Input.IsNew); @@ -108,27 +133,25 @@ public class Hwma : AbstractBase { _pF = Input.Value; _pA = _pV = 0; + return Input.Value; } - double nA = _nA, nB = _nB, nC = _nC; if (_period == 1) { - nA = 1; - nB = 0; - nC = 0; + _pF = Input.Value; + _pV = _pA = 0; + return Input.Value; } - double F = (1 - nA) * (_pF + _pV + 0.5 * _pA) + nA * Input.Value; - double V = (1 - nB) * (_pV + _pA) + nB * (F - _pF); - double A = (1 - nC) * _pA + nC * (V - _pV); - - double hwma = F + V + 0.5 * A; + double F = CalculateLevel(Input.Value); + double V = CalculateVelocity(F); + double A = CalculateAcceleration(V); _pF = F; _pV = V; _pA = A; IsHot = _index >= WarmupPeriod; - return hwma; + return F + V + _halfA * A; } } diff --git a/lib/averages/Jma.cs b/lib/averages/Jma.cs index e250ca15..092e3607 100644 --- a/lib/averages/Jma.cs +++ b/lib/averages/Jma.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -32,12 +32,16 @@ public class Jma : AbstractBase private readonly double _phase; private readonly CircularBuffer _vsumBuff; private readonly CircularBuffer _avoltyBuff; - - private double _len1; - private double _pow1; private readonly double _beta; + private readonly double _len1; + private readonly double _pow1; + private readonly double _oneMinusAlpha; + private readonly double _oneMinusAlphaSquared; + private readonly double _alphaSquared; + private double _upperBand, _lowerBand, _p_upperBand, _p_lowerBand; - private double _prevMa1, _prevDet0, _prevDet1, _prevJma, _p_prevMa1, _p_prevDet0, _p_prevDet1, _p_prevJma; + private double _prevMa1, _prevDet0, _prevDet1, _prevJma; + private double _p_prevMa1, _p_prevDet0, _p_prevDet1, _p_prevJma; private double _vSum, _p_vSum; public double UpperBand { get; set; } @@ -45,57 +49,50 @@ public class Jma : AbstractBase public double Volty { get; set; } public double Factor { get; set; } - /// - /// Initializes a new instance of the Jma class with the specified parameters. - /// - /// The period over which to calculate the JMA. - /// The phase parameter (-100 to +100) controlling lag compensation. - /// The factor controlling volatility adaptation (default 0.45). - /// The size of the volatility buffer (default 10). - /// Thrown when period is less than 1. public Jma(int period, int phase = 0, double factor = 0.45, int buffer = 10) { if (period < 1) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); + throw new System.ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); } Factor = factor; _period = period; - _phase = Math.Clamp((phase * 0.01) + 1.5, 0.5, 2.5); + _phase = System.Math.Clamp((phase * 0.01) + 1.5, 0.5, 2.5); _vsumBuff = new CircularBuffer(buffer); _avoltyBuff = new CircularBuffer(65); - _beta = factor * (_period - 1) / (factor * (_period - 1) + 2); + _beta = factor * (period - 1) / (factor * (period - 1) + 2); + + _len1 = System.Math.Max((System.Math.Log(System.Math.Sqrt(period - 1)) / System.Math.Log(2.0)) + 2.0, 0); + _pow1 = System.Math.Max(_len1 - 2.0, 0.5); + + // Precalculate constants for alpha-based calculations + double alpha = System.Math.Pow(_beta, _pow1); + _oneMinusAlpha = 1.0 - alpha; + _oneMinusAlphaSquared = _oneMinusAlpha * _oneMinusAlpha; + _alphaSquared = alpha * alpha; WarmupPeriod = period * 2; Name = $"JMA({period})"; } - /// - /// Initializes a new instance of the Jma class with a specified source. - /// - /// The data source object that publishes updates. - /// The period over which to calculate the JMA. - /// The phase parameter (-100 to +100) controlling lag compensation. - /// The factor controlling volatility adaptation (default 0.45). - /// The size of the volatility buffer (default 10). public Jma(object source, int period, int phase = 0, double factor = 0.45, int buffer = 10) : this(period, phase, factor, buffer) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _upperBand = _lowerBand = 0.0; _p_upperBand = _p_lowerBand = 0.0; - _len1 = Math.Max((Math.Log(Math.Sqrt(_period - 1)) / Math.Log(2.0)) + 2.0, 0); - _pow1 = Math.Max(_len1 - 2.0, 0.5); _avoltyBuff.Clear(); _vsumBuff.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -121,6 +118,23 @@ public class Jma : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateVolatility(double price, double del1, double del2) + { + double volty = System.Math.Max(System.Math.Abs(del1), System.Math.Abs(del2)); + _vsumBuff.Add(volty, Input.IsNew); + _vSum += (_vsumBuff[^1] - _vsumBuff[0]) / _vsumBuff.Count; + _avoltyBuff.Add(_vSum, Input.IsNew); + return volty; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateRelativeVolatility(double volty, double avgVolty) + { + double rvolty = (avgVolty > 0) ? volty / avgVolty : 1; + return System.Math.Min(System.Math.Max(rvolty, 1.0), System.Math.Pow(_len1, 1.0 / _pow1)); + } + protected override double Calculation() { ManageState(Input.IsNew); @@ -130,35 +144,30 @@ public class Jma : AbstractBase { _upperBand = _lowerBand = price; _prevMa1 = _prevJma = price; + return price; } double del1 = price - _upperBand; double del2 = price - _lowerBand; - double volty = Math.Max(Math.Abs(del1), Math.Abs(del2)); + double volty = CalculateVolatility(price, del1, del2); + double avgVolty = _avoltyBuff.Average(); - _vsumBuff.Add(volty, Input.IsNew); - _vSum += (_vsumBuff[^1] - _vsumBuff[0]) / _vsumBuff.Count; - _avoltyBuff.Add(_vSum, Input.IsNew); - double avgvolty = _avoltyBuff.Average(); - - double rvolty = (avgvolty > 0) ? volty / avgvolty : 1; - rvolty = Math.Min(Math.Max(rvolty, 1.0), Math.Pow(_len1, 1.0 / _pow1)); - - double pow2 = Math.Pow(rvolty, _pow1); - double Kv = Math.Pow(_beta, Math.Sqrt(pow2)); + double rvolty = CalculateRelativeVolatility(volty, avgVolty); + double pow2 = System.Math.Pow(rvolty, _pow1); + double Kv = System.Math.Pow(_beta, System.Math.Sqrt(pow2)); _upperBand = (del1 >= 0) ? price : price - (Kv * del1); _lowerBand = (del2 <= 0) ? price : price - (Kv * del2); - double _alpha = Math.Pow(_beta, pow2); - double ma1 = Input.Value + _alpha * (_prevMa1 - Input.Value); + double alpha = System.Math.Pow(_beta, pow2); + double ma1 = price + alpha * (_prevMa1 - price); _prevMa1 = ma1; double det0 = price + _beta * (_prevDet0 - price + ma1) - ma1; _prevDet0 = det0; double ma2 = ma1 + _phase * det0; - double det1 = ((ma2 - _prevJma) * (1 - _alpha) * (1 - _alpha)) + (_alpha * _alpha * _prevDet1); + double det1 = ((ma2 - _prevJma) * _oneMinusAlphaSquared) + (_alphaSquared * _prevDet1); _prevDet1 = det1; double jma = _prevJma + det1; _prevJma = jma; diff --git a/lib/averages/Kama.cs b/lib/averages/Kama.cs index 7c15a827..1bd625c1 100644 --- a/lib/averages/Kama.cs +++ b/lib/averages/Kama.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -29,7 +29,8 @@ public class Kama : AbstractBase { private readonly int _period; private readonly double _scFast, _scSlow; - private CircularBuffer? _buffer; + private readonly double _scDiff; // Precalculated (_scFast - _scSlow) + private readonly CircularBuffer _buffer; private double _lastKama, _p_lastKama; /// The number of periods used to calculate the Efficiency Ratio. @@ -40,11 +41,13 @@ public class Kama : AbstractBase { if (period < 1) { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } _period = period; _scFast = 2.0 / (((period < fast) ? period : fast) + 1); _scSlow = 2.0 / (slow + 1); + _scDiff = _scFast - _scSlow; + _buffer = new CircularBuffer(_period + 1); WarmupPeriod = period; Name = $"Kama({_period}, {fast}, {slow})"; Init(); @@ -60,13 +63,15 @@ public class Kama : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); - _buffer = new CircularBuffer(_period + 1); + _buffer.Clear(); _lastKama = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -81,36 +86,50 @@ public class Kama : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateVolatility() + { + double volatility = 0; + for (int i = 1; i < _buffer.Count; i++) + { + volatility += System.Math.Abs(_buffer[i] - _buffer[i - 1]); + } + return volatility; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateEfficiencyRatio(double change, double volatility) + { + return volatility != 0 ? change / volatility : 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateSmoothingConstant(double er) + { + double sc = (er * _scDiff) + _scSlow; + return sc * sc; // Square the smoothing constant + } + protected override double Calculation() { ManageState(Input.IsNew); - _buffer!.Add(Input.Value, Input.IsNew); + _buffer.Add(Input.Value, Input.IsNew); - double kama; if (_index <= _period) { - kama = Input.Value; - } - else - { - double change = Math.Abs(_buffer[^1] - _buffer[0]); - double volatility = 0; - for (int i = 1; i < _buffer.Count; i++) - { - volatility += Math.Abs(_buffer[i] - _buffer[i - 1]); - } - - double er = volatility != 0 ? change / volatility : 0; - double sc = (er * (_scFast - _scSlow)) + _scSlow; - sc *= sc; // Square the smoothing constant - - kama = _lastKama + (sc * (Input.Value - _lastKama)); + _lastKama = Input.Value; + return Input.Value; } - _lastKama = kama; + double change = System.Math.Abs(_buffer[^1] - _buffer[0]); + double volatility = CalculateVolatility(); + double er = CalculateEfficiencyRatio(change, volatility); + double sc = CalculateSmoothingConstant(er); + + _lastKama += sc * (Input.Value - _lastKama); IsHot = _index >= WarmupPeriod; - return kama; + return _lastKama; } } diff --git a/lib/averages/Ltma.cs b/lib/averages/Ltma.cs index 910322ab..dd7c8415 100644 --- a/lib/averages/Ltma.cs +++ b/lib/averages/Ltma.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -29,6 +29,8 @@ namespace QuanTAlib; public class Ltma : AbstractBase { private readonly double _gamma; + private readonly double _oneMinusGamma; + private readonly double _invSix = 1.0 / 6.0; // Precalculated constant for final averaging private double _prevL0, _prevL1, _prevL2, _prevL3; private double _p_prevL0, _p_prevL1, _p_prevL2, _p_prevL3; @@ -42,8 +44,9 @@ public class Ltma : AbstractBase public Ltma(double gamma = 0.1) { if (gamma < 0 || gamma > 1) - throw new ArgumentOutOfRangeException(nameof(gamma), "Gamma must be between 0 and 1."); + throw new System.ArgumentOutOfRangeException(nameof(gamma), "Gamma must be between 0 and 1."); _gamma = gamma; + _oneMinusGamma = 1.0 - gamma; Name = $"Laguerre({gamma:F2})"; WarmupPeriod = 4; // Minimum number of samples needed Init(); @@ -57,12 +60,14 @@ public class Ltma : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _prevL0 = _prevL1 = _prevL2 = _prevL3 = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -82,24 +87,37 @@ public class Ltma : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateLaguerreStage(double input, double prev, double prevPrev) + { + return -_gamma * input + prev + _gamma * prevPrev; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CombineOutputs(double l0, double l1, double l2, double l3) + { + return (l0 + 2.0 * (l1 + l2) + l3) * _invSix; + } + protected override double Calculation() { ManageState(Input.IsNew); - // Laguerre filter calculation - double _l0 = (1 - _gamma) * Input.Value + _gamma * _prevL0; - double _l1 = -_gamma * _l0 + _prevL0 + _gamma * _prevL1; - double _l2 = -_gamma * _l1 + _prevL1 + _gamma * _prevL2; - double _l3 = -_gamma * _l2 + _prevL2 + _gamma * _prevL3; - _prevL0 = _l0; - _prevL1 = _l1; - _prevL2 = _l2; - _prevL3 = _l3; + // First stage + double l0 = _oneMinusGamma * Input.Value + _gamma * _prevL0; - double filteredValue = (_l0 + 2 * _l1 + 2 * _l2 + _l3) / 6; + // Subsequent stages using helper method + double l1 = CalculateLaguerreStage(l0, _prevL0, _prevL1); + double l2 = CalculateLaguerreStage(l1, _prevL1, _prevL2); + double l3 = CalculateLaguerreStage(l2, _prevL2, _prevL3); + + // Store values for next iteration + _prevL0 = l0; + _prevL1 = l1; + _prevL2 = l2; + _prevL3 = l3; IsHot = _index >= WarmupPeriod; - - return filteredValue; + return CombineOutputs(l0, l1, l2, l3); } } diff --git a/lib/averages/Maaf.cs b/lib/averages/Maaf.cs index b3ddc808..11c0fb81 100644 --- a/lib/averages/Maaf.cs +++ b/lib/averages/Maaf.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -33,11 +32,13 @@ public class Maaf : AbstractBase { private readonly CircularBuffer _priceBuffer; private readonly CircularBuffer _smoothBuffer; - private double _prevFilter, _prevValue2; private readonly double _threshold; - private double _p_prevFilter, _p_prevValue2; - private readonly int _period; + private readonly double _invSix = 1.0 / 6.0; + private readonly double[] _sortBuffer; // Pre-allocated buffer for sorting + + private double _prevFilter, _prevValue2; + private double _p_prevFilter, _p_prevValue2; /// The initial period for the filter (default 39). /// The threshold for adaptive adjustment (default 0.002). @@ -47,6 +48,7 @@ public class Maaf : AbstractBase _threshold = threshold; _priceBuffer = new CircularBuffer(4); _smoothBuffer = new CircularBuffer(period); + _sortBuffer = new double[period]; // Pre-allocate sorting buffer Name = "MAAF"; WarmupPeriod = period; Init(); @@ -61,15 +63,17 @@ public class Maaf : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { + base.Init(); _priceBuffer.Clear(); _smoothBuffer.Clear(); _prevFilter = 0; _prevValue2 = 0; - base.Init(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -86,6 +90,30 @@ public class Maaf : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateSmooth() + { + return (_priceBuffer[^1] + 2.0 * (_priceBuffer[^2] + _priceBuffer[^3]) + _priceBuffer[^4]) * _invSix; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double GetMedian(int length) + { + // Copy values to pre-allocated buffer + var span = _smoothBuffer.GetSpan().Slice(_smoothBuffer.Count - length, length); + span.CopyTo(_sortBuffer.AsSpan(0, length)); + + // Sort the required portion + System.Array.Sort(_sortBuffer, 0, length); + return _sortBuffer[length / 2]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double CalculateAlpha(int length) + { + return 2.0 / (length + 1); + } + protected override double Calculation() { ManageState(IsNew); @@ -97,7 +125,7 @@ public class Maaf : AbstractBase return Input.Value; } - double smooth = (_priceBuffer[^1] + (2 * _priceBuffer[^2]) + (2 * _priceBuffer[^3]) + _priceBuffer[^4]) / 6; + double smooth = CalculateSmooth(); _smoothBuffer.Add(smooth, Input.IsNew); if (_smoothBuffer.Count < _period) @@ -111,28 +139,23 @@ public class Maaf : AbstractBase while (value3 > _threshold && length > 0) { - double alpha = 2.0 / (length + 1); - - var sortedValues = _smoothBuffer.TakeLast(length).OrderBy(x => x).ToList(); - double value1 = sortedValues[length / 2]; + double alpha = CalculateAlpha(length); + double value1 = GetMedian(length); value2 = alpha * (smooth - _prevValue2) + _prevValue2; if (value1 != 0) { - value3 = Math.Abs(value1 - value2) / value1; + value3 = System.Math.Abs(value1 - value2) / value1; } length -= 2; } - if (length < 3) length = 3; - - double finalAlpha = 2.0 / (length + 1); + length = System.Math.Max(length, 3); + double finalAlpha = CalculateAlpha(length); double filter = finalAlpha * (smooth - _prevFilter) + _prevFilter; - _p_prevFilter = _prevFilter; _prevFilter = filter; - _p_prevValue2 = _prevValue2; _prevValue2 = value2; IsHot = _index >= WarmupPeriod; diff --git a/lib/averages/Mama.cs b/lib/averages/Mama.cs index 74e82a73..3722ab90 100644 --- a/lib/averages/Mama.cs +++ b/lib/averages/Mama.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -31,6 +31,12 @@ public class Mama : AbstractBase { private readonly double _fastLimit, _slowLimit; private readonly CircularBuffer _pr, _sm, _dt, _i1, _q1, _i2, _q2, _re, _im, _pd, _ph; + private readonly double _twoPi = 2.0 * System.Math.PI; + private readonly double _radToDeg = 180.0 / System.Math.PI; + private readonly double _alpha02 = 0.2; + private readonly double _alpha08 = 0.8; + private readonly double _famaAlpha = 0.5; + private double _mama, _fama; private double _prevMama, _prevFama, _sumPr; private double _p_prevMama, _p_prevFama, _p_sumPr; @@ -40,12 +46,9 @@ public class Mama : AbstractBase /// public TValue Fama { get; private set; } - /// The maximum adaptation speed (default 0.5). - /// The minimum adaptation speed (default 0.05). public Mama(double fastLimit = 0.5, double slowLimit = 0.05) { Fama = new TValue(); - Name = $"Mama({_fastLimit:F2}, {_slowLimit:F2})"; _fastLimit = fastLimit; _slowLimit = slowLimit; _pr = new(7); @@ -59,23 +62,23 @@ public class Mama : AbstractBase _im = new(2); _pd = new(2); _ph = new(2); + Name = $"Mama({_fastLimit:F2}, {_slowLimit:F2})"; Init(); } - /// The data source object that publishes updates. - /// The maximum adaptation speed (default 0.5). - /// The minimum adaptation speed (default 0.05). public Mama(object source, double fastLimit = 0.5, double slowLimit = 0.05) : this(fastLimit, slowLimit) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { Fama = new TValue(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -94,6 +97,33 @@ public class Mama : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateSmooth() + { + return (4.0 * _pr[^1] + 3.0 * _pr[^2] + 2.0 * _pr[^3] + _pr[^4]) * 0.1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double CalculateHilbertTransform(CircularBuffer buffer, double adj) + { + return (0.0962 * (buffer[^1] - buffer[^7]) + 0.5769 * (buffer[^3] - buffer[^5])) * adj; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculatePeriod(double im, double re) + { + if (im == 0 || re == 0) return _pd[^2]; + return _twoPi / System.Math.Atan(im / re); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double AdjustPeriod(double period) + { + period = System.Math.Clamp(period, 0.67 * _pd[^2], 1.5 * _pd[^2]); + period = System.Math.Clamp(period, 6.0, 50.0); + return _alpha02 * period + _alpha08 * _pd[^2]; + } + protected override double Calculation() { ManageState(Input.IsNew); @@ -102,85 +132,59 @@ public class Mama : AbstractBase if (_index > 6) { - double adj = (0.075 * _pd[^1]) + 0.54; + double adj = 0.075 * _pd[^1] + 0.54; - // Smooth - _sm.Add(((4 * _pr[^1]) + (3 * _pr[^2]) + (2 * _pr[^3]) + _pr[^4]) / 10, Input.IsNew); - - // Detrender - _dt.Add(((0.0962 * _sm[^1]) + (0.5769 * _sm[^3]) - (0.5769 * _sm[^5]) - (0.0962 * _sm[^7])) * adj, Input.IsNew); + // Smooth and Detrender + _sm.Add(CalculateSmooth(), Input.IsNew); + _dt.Add(CalculateHilbertTransform(_sm, adj), Input.IsNew); // In-phase and quadrature - _q1.Add(((0.0962 * _dt[^1]) + (0.5769 * _dt[^3]) - (0.5769 * _dt[^5]) - (0.0962 * _dt[^7])) * adj, Input.IsNew); + _q1.Add(CalculateHilbertTransform(_dt, adj), Input.IsNew); _i1.Add(_dt[^4], Input.IsNew); - // Advance the phases by 90 degrees - double jI = ((0.0962 * _i1[^1]) + (0.5769 * _i1[^3]) - (0.5769 * _i1[^5]) - (0.0962 * _i1[^7])) * adj; - double jQ = ((0.0962 * _q1[^1]) + (0.5769 * _q1[^3]) - (0.5769 * _q1[^5]) - (0.0962 * _q1[^7])) * adj; + // Advance phases + double jI = CalculateHilbertTransform(_i1, adj); + double jQ = CalculateHilbertTransform(_q1, adj); - // Phasor addition for 3-bar averaging - _i2.Add(_i1[^1] - jQ, Input.IsNew); - _q2.Add(_q1[^1] + jI, Input.IsNew); - _i2[^1] = 0.2 * _i2[^1] + 0.8 * _i2[^2]; - _q2[^1] = 0.2 * _q2[^1] + 0.8 * _q2[^2]; + // Phasor addition + double i2 = _i1[^1] - jQ; + double q2 = _q1[^1] + jI; + _i2.Add(i2, Input.IsNew); + _q2.Add(q2, Input.IsNew); + _i2[^1] = _alpha02 * _i2[^1] + _alpha08 * _i2[^2]; + _q2[^1] = _alpha02 * _q2[^1] + _alpha08 * _q2[^2]; // Homodyne discriminator - _re.Add((_i2[^1] * _i2[^2]) + (_q2[^1] * _q2[^2]), Input.IsNew); - _im.Add((_i2[^1] * _q2[^2]) - (_q2[^1] * _i2[^2]), Input.IsNew); - _re[^1] = (0.2 * _re[^1]) + (0.8 * _re[^2]); - _im[^1] = (0.2 * _im[^1]) + (0.8 * _im[^2]); + double re = _i2[^1] * _i2[^2] + _q2[^1] * _q2[^2]; + double im = _i2[^1] * _q2[^2] - _q2[^1] * _i2[^2]; + _re.Add(re, Input.IsNew); + _im.Add(im, Input.IsNew); + _re[^1] = _alpha02 * _re[^1] + _alpha08 * _re[^2]; + _im[^1] = _alpha02 * _im[^1] + _alpha08 * _im[^2]; - // Calculate period - if (_im[^1] != 0 && _re[^1] != 0) - { - _pd.Add(2 * Math.PI / Math.Atan(_im[^1] / _re[^1]), Input.IsNew); - } - else - { - _pd.Add(_pd[^2], Input.IsNew); - } + // Calculate and adjust period + double period = CalculatePeriod(_im[^1], _re[^1]); + _pd.Add(period, Input.IsNew); + _pd[^1] = AdjustPeriod(_pd[^1]); - // Adjust period to thresholds - _pd[^1] = Math.Max(Math.Min(_pd[^1], 1.5 * _pd[^2]), 0.67 * _pd[^2]); - _pd[^1] = Math.Max(Math.Min(_pd[^1], 50), 6); - _pd[^1] = (0.2 * _pd[^1]) + (0.8 * _pd[^2]); + // Phase calculation + double phase = _i1[^1] != 0 ? System.Math.Atan(_q1[^1] / _i1[^1]) * _radToDeg : _ph[^2]; + _ph.Add(phase, Input.IsNew); - // Determine phase position - if (_i1[^1] != 0) - { - _ph.Add(Math.Atan(_q1[^1] / _i1[^1]) * 180 / Math.PI, Input.IsNew); - } - else - { - _ph.Add(_ph[^2], Input.IsNew); - } - - // Change in phase - double delta = Math.Max(_ph[^2] - _ph[^1], 1); - - // Adaptive alpha value - double alpha = Math.Max(_fastLimit / delta, _slowLimit); + // Adaptive alpha + double delta = System.Math.Max(_ph[^2] - _ph[^1], 1.0); + double alpha = System.Math.Clamp(_fastLimit / delta, _slowLimit, _fastLimit); // Final indicators _mama = alpha * (_pr[^1] - _prevMama) + _prevMama; - _fama = 0.5 * alpha * (_mama - _prevFama) + _prevFama; + _fama = _famaAlpha * alpha * (_mama - _prevFama) + _prevFama; _prevMama = _mama; _prevFama = _fama; } else { - _pd.Add(0, Input.IsNew); - _sm.Add(0, Input.IsNew); - _dt.Add(0, Input.IsNew); - _i1.Add(0, Input.IsNew); - _q1.Add(0, Input.IsNew); - _i2.Add(0, Input.IsNew); - _q2.Add(0, Input.IsNew); - _re.Add(0, Input.IsNew); - _im.Add(0, Input.IsNew); - _ph.Add(0, Input.IsNew); - + InitializeBuffers(); _sumPr += Input.Value; _mama = _fama = _prevMama = _prevFama = _sumPr / _index; } @@ -190,4 +194,19 @@ public class Mama : AbstractBase return _mama; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void InitializeBuffers() + { + _pd.Add(0, Input.IsNew); + _sm.Add(0, Input.IsNew); + _dt.Add(0, Input.IsNew); + _i1.Add(0, Input.IsNew); + _q1.Add(0, Input.IsNew); + _i2.Add(0, Input.IsNew); + _q2.Add(0, Input.IsNew); + _re.Add(0, Input.IsNew); + _im.Add(0, Input.IsNew); + _ph.Add(0, Input.IsNew); + } } diff --git a/lib/averages/Mgdi.cs b/lib/averages/Mgdi.cs index 70ec070c..efa64a61 100644 --- a/lib/averages/Mgdi.cs +++ b/lib/averages/Mgdi.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -29,6 +29,7 @@ public class Mgdi : AbstractBase { private readonly int _period; private readonly double _kFactor; + private readonly double _kFactorPeriod; // Precalculated k * period private double _prevMd, _p_prevMd; /// The number of periods used in the MGDI calculation. @@ -38,14 +39,15 @@ public class Mgdi : AbstractBase { if (period <= 0) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0."); + throw new System.ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0."); } if (kFactor <= 0) { - throw new ArgumentOutOfRangeException(nameof(kFactor), "K-Factor must be greater than 0."); + throw new System.ArgumentOutOfRangeException(nameof(kFactor), "K-Factor must be greater than 0."); } _period = period; _kFactor = kFactor; + _kFactorPeriod = kFactor * period; Name = "Mgdi"; WarmupPeriod = period; Init(); @@ -60,12 +62,14 @@ public class Mgdi : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _prevMd = _p_prevMd = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -79,6 +83,18 @@ public class Mgdi : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateRatio(double value) + { + return _prevMd != 0 ? value / _prevMd : 1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateMd(double value, double ratio) + { + return _prevMd + ((value - _prevMd) / (_kFactorPeriod * System.Math.Pow(ratio, 4))); + } + protected override double Calculation() { ManageState(Input.IsNew); @@ -90,10 +106,8 @@ public class Mgdi : AbstractBase } else { - double ratio = _prevMd != 0 ? value / _prevMd : 1; - double md = _prevMd + ((value - _prevMd) / - (_kFactor * _period * Math.Pow(ratio, 4))); - _prevMd = md; + double ratio = CalculateRatio(value); + _prevMd = CalculateMd(value, ratio); } IsHot = _index >= _period; diff --git a/lib/averages/Mma.cs b/lib/averages/Mma.cs index 99d92ab3..c999ee09 100644 --- a/lib/averages/Mma.cs +++ b/lib/averages/Mma.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -29,6 +29,9 @@ public class Mma : AbstractBase { private readonly int _period; private readonly CircularBuffer _buffer; + private readonly double _periodRecip; // 1/period + private readonly double _combinedRecip; // 6/((period+1)*period) + private readonly double[] _weights; // Precalculated weights private double _lastMma; /// The number of periods used in the MMA calculation. Must be at least 2. @@ -37,10 +40,20 @@ public class Mma : AbstractBase { if (period < 2) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2."); + throw new System.ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2."); } _period = period; _buffer = new CircularBuffer(period); + _periodRecip = 1.0 / period; + _combinedRecip = 6.0 / ((period + 1) * period); + + // Precalculate weights + _weights = new double[period]; + for (int i = 0; i < period; i++) + { + _weights[i] = (period - (2 * i + 1)) * 0.5; + } + Name = "Mma"; WarmupPeriod = period; Init(); @@ -54,6 +67,7 @@ public class Mma : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -61,6 +75,7 @@ public class Mma : AbstractBase _buffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -69,6 +84,17 @@ public class Mma : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateWeightedSum() + { + double sum = 0; + for (int i = 0; i < _period; i++) + { + sum += _weights[i] * _buffer[^(i + 1)]; + } + return sum; + } + protected override double Calculation() { ManageState(Input.IsNew); @@ -78,7 +104,7 @@ public class Mma : AbstractBase { double T = _buffer.Sum(); double S = CalculateWeightedSum(); - _lastMma = (T / _period) + (6 * S) / ((_period + 1) * _period); + _lastMma = (T * _periodRecip) + (S * _combinedRecip); } else { @@ -89,20 +115,4 @@ public class Mma : AbstractBase IsHot = _index >= _period; return _lastMma; } - - /// - /// Calculates the weighted sum component of the MMA. - /// The weights are symmetric around the center, decreasing linearly from the center outward. - /// - /// The weighted sum of the data points. - private double CalculateWeightedSum() - { - double sum = 0; - for (int i = 0; i < _period; i++) - { - double weight = (_period - (2 * i + 1)) / 2.0; - sum += weight * _buffer[^(i + 1)]; - } - return sum; - } } diff --git a/lib/averages/Pwma.cs b/lib/averages/Pwma.cs index 73c27470..c06b82f0 100644 --- a/lib/averages/Pwma.cs +++ b/lib/averages/Pwma.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -31,6 +30,7 @@ public class Pwma : AbstractBase { private readonly int _period; private readonly Convolution _convolution; + private readonly double[] _kernel; /// The number of data points used in the PWMA calculation. /// Thrown when period is less than 1. @@ -38,10 +38,11 @@ public class Pwma : AbstractBase { if (period < 1) { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } _period = period; - _convolution = new Convolution(GenerateKernel(_period)); + _kernel = GenerateKernel(_period); + _convolution = new Convolution(_kernel); Name = "Pwma"; WarmupPeriod = period; Init(); @@ -55,12 +56,14 @@ public class Pwma : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private new void Init() { base.Init(); _convolution.Init(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -70,24 +73,33 @@ public class Pwma : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double CalculateKernelSum(double[] kernel, int length) + { + double sum = 0; + for (int i = 0; i < length; i++) + { + sum += kernel[i]; + } + return sum; + } + protected override double Calculation() { ManageState(Input.IsNew); // Use Convolution for calculation - TValue convolutionResult = _convolution.Calc(Input); - + var convolutionResult = _convolution.Calc(Input); double result = convolutionResult.Value; // Adjust for partial periods during warmup if (_index < _period) { double[] partialKernel = GenerateKernel(_index); - result /= partialKernel.Sum(); + result *= CalculateKernelSum(_kernel, _period) / CalculateKernelSum(partialKernel, _index); } IsHot = _index >= WarmupPeriod; - return result; } @@ -96,11 +108,13 @@ public class Pwma : AbstractBase /// /// The period for which to generate the kernel. /// An array of normalized Pascal's triangle-based weights for the convolution operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double[] GenerateKernel(int period) { double[] kernel = new double[period]; kernel[0] = 1; + // Generate Pascal's triangle coefficients for (int i = 1; i < period; i++) { for (int j = i; j > 0; j--) @@ -109,11 +123,13 @@ public class Pwma : AbstractBase } } - // Normalize the kernel - double weightSum = kernel.Sum(); + // Calculate sum and normalize in one pass + double weightSum = CalculateKernelSum(kernel, period); + double invWeightSum = 1.0 / weightSum; + for (int i = 0; i < period; i++) { - kernel[i] /= weightSum; + kernel[i] *= invWeightSum; } return kernel; diff --git a/lib/averages/Qema.cs b/lib/averages/Qema.cs index cdf08f82..66cc7107 100644 --- a/lib/averages/Qema.cs +++ b/lib/averages/Qema.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -41,7 +41,7 @@ public class Qema : AbstractBase { if (k1 <= 0 || k2 <= 0 || k3 <= 0 || k4 <= 0) { - throw new ArgumentOutOfRangeException(nameof(k1), "All k values must be in the range (0, 1]."); + throw new System.ArgumentOutOfRangeException(nameof(k1), "All k values must be in the range (0, 1]."); } _ema1 = new Ema(k1); @@ -50,8 +50,7 @@ public class Qema : AbstractBase _ema4 = new Ema(k4); Name = $"QEMA ({k1:F2},{k2:F2},{k3:F2},{k4:F2})"; - double smK = Math.Min(Math.Min(k1, k2), Math.Min(k3, k4)); - + double smK = System.Math.Min(System.Math.Min(k1, k2), System.Math.Min(k3, k4)); WarmupPeriod = (int)((2 - smK) / smK); Init(); } @@ -68,6 +67,7 @@ public class Qema : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -75,6 +75,7 @@ public class Qema : AbstractBase _p_lastQema = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -88,16 +89,25 @@ public class Qema : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateEma(Ema ema, double value) + { + var tempValue = new TValue(Input.Time, value, Input.IsNew); + return ema.Calc(tempValue).Value; + } + protected override double Calculation() { ManageState(Input.IsNew); - double ema1 = _ema1.Calc(new TValue(Input.Time, Input.Value, Input.IsNew)); - double ema2 = _ema2.Calc(new TValue(Input.Time, ema1, Input.IsNew)); - double ema3 = _ema3.Calc(new TValue(Input.Time, ema2, Input.IsNew)); - double ema4 = _ema4.Calc(new TValue(Input.Time, ema3, Input.IsNew)); + // Calculate EMAs in sequence + double ema1 = CalculateEma(_ema1, Input.Value); + double ema2 = CalculateEma(_ema2, ema1); + double ema3 = CalculateEma(_ema3, ema2); + double ema4 = CalculateEma(_ema4, ema3); - _lastQema = 4 * ema1 - 6 * ema2 + 4 * ema3 - ema4; + // Combine EMAs using optimized formula + _lastQema = 4.0 * (ema1 + ema3) - (6.0 * ema2 + ema4); IsHot = _index >= WarmupPeriod; return _lastQema; diff --git a/lib/averages/Rema.cs b/lib/averages/Rema.cs index b8420a3d..65c76f70 100644 --- a/lib/averages/Rema.cs +++ b/lib/averages/Rema.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -29,6 +29,7 @@ public class Rema : AbstractBase { private readonly int _period; private readonly double _lambda; + private readonly double _lambdaPlus1Recip; // 1/(1 + lambda) private double _lastRema, _prevRema; private double _savedLastRema, _savedPrevRema; @@ -48,12 +49,13 @@ public class Rema : AbstractBase public Rema(int period, double lambda = 0.5) { if (period < 1) - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); + throw new System.ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); if (lambda < 0) - throw new ArgumentOutOfRangeException(nameof(lambda), "Lambda must be non-negative."); + throw new System.ArgumentOutOfRangeException(nameof(lambda), "Lambda must be non-negative."); _period = period; _lambda = lambda; + _lambdaPlus1Recip = 1.0 / (1.0 + lambda); Name = $"REMA({period},{lambda:F2})"; WarmupPeriod = period; Init(); @@ -68,6 +70,7 @@ public class Rema : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -77,6 +80,7 @@ public class Rema : AbstractBase _savedPrevRema = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -92,15 +96,28 @@ public class Rema : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateAlpha() + { + return 2.0 / (System.Math.Min(_period, _index) + 1); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateRema(double alpha, double input) + { + double standardTerm = _lastRema + alpha * (input - _lastRema); + double regularizationTerm = _lastRema + (_lastRema - _prevRema); + return (standardTerm + _lambda * regularizationTerm) * _lambdaPlus1Recip; + } + protected override double Calculation() { ManageState(Input.IsNew); - double alpha = 2.0 / (Math.Min(_period, _index) + 1); - if (_index > 2) { - double rema = (_lastRema + alpha * (Input.Value - _lastRema) + _lambda * (_lastRema + (_lastRema - _prevRema))) / (1 + _lambda); + double alpha = CalculateAlpha(); + double rema = CalculateRema(alpha, Input.Value); _prevRema = _lastRema; _lastRema = rema; } @@ -110,7 +127,7 @@ public class Rema : AbstractBase _lastRema = Input.Value; } else - { // _index == 1 + { _lastRema = Input.Value; } diff --git a/lib/averages/Rma.cs b/lib/averages/Rma.cs index 628d8310..71dc6a76 100644 --- a/lib/averages/Rma.cs +++ b/lib/averages/Rma.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -20,44 +21,17 @@ namespace QuanTAlib; /// public class Rma : AbstractBase { - // inherited _index - // inherited _value - - /// - /// The period for the RMA calculation. - /// private readonly int _period; - - /// - /// Circular buffer for SMA calculation. - /// + private readonly double _k; // Wilder's smoothing factor + private readonly double _oneMinusK; // 1 - k + private readonly double _epsilon = 1e-10; + private readonly bool _useSma; private CircularBuffer _sma; - /// - /// The last calculated RMA value. - /// private double _lastRma, _p_lastRma; - - /// - /// Compensator for early RMA values. - /// private double _e, _p_e; - - /// - /// The smoothing factor for RMA calculation. - /// - private readonly double _k; - - /// - /// Flags to track initialization status. - /// private bool _isInit, _p_isInit; - /// - /// Flag to determine whether to use SMA for initial values. - /// - private readonly bool _useSma; - /// /// Initializes a new instance of the Rma class with a specified period. /// @@ -68,14 +42,15 @@ public class Rma : AbstractBase { if (period < 1) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); + throw new System.ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); } _period = period; - _k = 1.0 / _period; // Wilder's smoothing factor + _k = 1.0 / period; + _oneMinusK = 1.0 - _k; _useSma = useSma; _sma = new(period); Name = "Rma"; - WarmupPeriod = _period * 2; // RMA typically needs more warmup periods + WarmupPeriod = period * 2; // RMA typically needs more warmup periods Init(); } @@ -91,9 +66,7 @@ public class Rma : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } - /// - /// Initializes the Rma instance. - /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -104,10 +77,7 @@ public class Rma : AbstractBase _sma = new(_period); } - /// - /// Manages the state of the Rma instance. - /// - /// Indicates whether the input is new. + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -125,21 +95,30 @@ public class Rma : AbstractBase } } - /// - /// Performs the RMA calculation. - /// - /// The calculated RMA value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateRma(double input) + { + return _k * input + _oneMinusK * _lastRma; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CompensateRma(double rma) + { + _e = (_e > _epsilon) ? _oneMinusK * _e : 0; + return (_useSma || _e <= double.Epsilon) ? rma : rma / (1.0 - _e); + } + protected override double Calculation() { - double result, _rma; ManageState(Input.IsNew); - // when _UseSma == true, use SMA calculation until we have enough data points + double result; if (!_isInit && _useSma) { _sma.Add(Input.Value, Input.IsNew); - _rma = _sma.Average(); - result = _rma; + _lastRma = _sma.Average(); + result = _lastRma; + if (_index >= _period) { _isInit = true; @@ -147,15 +126,10 @@ public class Rma : AbstractBase } else { - // compensator for early rma values - _e = (_e > 1e-10) ? (1 - _k) * _e : 0; - - _rma = _k * Input.Value + (1 - _k) * _lastRma; - - // _useSma decides if we use compensator or not - result = (_useSma || _e <= double.Epsilon) ? _rma : _rma / (1 - _e); + _lastRma = CalculateRma(Input.Value); + result = CompensateRma(_lastRma); } - _lastRma = _rma; + IsHot = _index >= WarmupPeriod; return result; } diff --git a/lib/averages/Sinema.cs b/lib/averages/Sinema.cs index 64efde2e..a58d59b3 100644 --- a/lib/averages/Sinema.cs +++ b/lib/averages/Sinema.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -29,6 +29,7 @@ namespace QuanTAlib; public class Sinema : AbstractBase { private readonly Convolution _convolution; + private readonly double[] _kernel; /// The number of data points used in the SINEMA calculation. /// Thrown when period is less than 1. @@ -36,9 +37,10 @@ public class Sinema : AbstractBase { if (period < 1) { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } - _convolution = new Convolution(GenerateKernel(period)); + _kernel = GenerateKernel(period); + _convolution = new Convolution(_kernel); Name = "Sinema"; WarmupPeriod = period; Init(); @@ -52,12 +54,14 @@ public class Sinema : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private new void Init() { base.Init(); _convolution.Init(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -67,42 +71,43 @@ public class Sinema : AbstractBase } } - protected override double Calculation() - { - ManageState(Input.IsNew); - - // Use Convolution for calculation - TValue convolutionResult = _convolution.Calc(Input); - - double result = convolutionResult.Value; - IsHot = _index >= WarmupPeriod; - - return result; - } - /// /// Generates the sine-based convolution kernel for the SINEMA calculation. /// /// The period for which to generate the kernel. /// An array of normalized sine-based weights for the convolution operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double[] GenerateKernel(int period) { double[] kernel = new double[period]; double weightSum = 0; + double piDivPeriodPlus1 = System.Math.PI / (period + 1); + // Calculate weights and sum in one pass for (int i = 0; i < period; i++) { - // Use sine function to generate weights - kernel[i] = Math.Sin((i + 1) * Math.PI / (period + 1)); + kernel[i] = System.Math.Sin((i + 1) * piDivPeriodPlus1); weightSum += kernel[i]; } - // Normalize the kernel + // Normalize using multiplication instead of division + double invWeightSum = 1.0 / weightSum; for (int i = 0; i < period; i++) { - kernel[i] /= weightSum; + kernel[i] *= invWeightSum; } return kernel; } + + protected override double Calculation() + { + ManageState(Input.IsNew); + + // Use Convolution for calculation + var convolutionResult = _convolution.Calc(Input); + IsHot = _index >= WarmupPeriod; + + return convolutionResult.Value; + } } diff --git a/lib/averages/Sma.cs b/lib/averages/Sma.cs index 6c797162..784abfd7 100644 --- a/lib/averages/Sma.cs +++ b/lib/averages/Sma.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -27,9 +27,8 @@ namespace QuanTAlib; public class Sma : AbstractBase { - // inherited _index - // inherited _value private readonly CircularBuffer _buffer; + private readonly int _period; /// The number of data points used in the SMA calculation. /// Thrown when period is less than 1. @@ -37,9 +36,9 @@ public class Sma : AbstractBase { if (period < 1) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); + throw new System.ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); } - WarmupPeriod = period; + _period = period; _buffer = new CircularBuffer(period); Name = "Sma"; WarmupPeriod = period; @@ -48,12 +47,13 @@ public class Sma : AbstractBase /// The data source object that publishes updates. /// The number of data points used in the SMA calculation. - public Sma(object source, int period) : this(period: period) + public Sma(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -69,12 +69,10 @@ public class Sma : AbstractBase /// The calculated SMA value. protected override double Calculation() { - double result; ManageState(IsNew); _buffer.Add(Input.Value, Input.IsNew); - result = _buffer.Average(); IsHot = _index >= WarmupPeriod; - return result; + return _buffer.Average(); } } diff --git a/lib/averages/Smma.cs b/lib/averages/Smma.cs index f66262b0..a508acff 100644 --- a/lib/averages/Smma.cs +++ b/lib/averages/Smma.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -28,7 +28,9 @@ namespace QuanTAlib; public class Smma : AbstractBase { private readonly int _period; - private CircularBuffer? _buffer; + private readonly double _periodRecip; // 1/period + private readonly double _periodMinusOne; // period-1 + private readonly CircularBuffer _buffer; private double _lastSmma, _p_lastSmma; /// The number of data points used in the SMMA calculation. @@ -37,9 +39,12 @@ public class Smma : AbstractBase { if (period < 1) { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } _period = period; + _periodRecip = 1.0 / period; + _periodMinusOne = period - 1; + _buffer = new CircularBuffer(period); WarmupPeriod = period; Name = $"Smma({_period})"; Init(); @@ -53,13 +58,15 @@ public class Smma : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); - _buffer = new CircularBuffer(_period); + _buffer.Clear(); _lastSmma = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -74,18 +81,21 @@ public class Smma : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateSmma(double input) + { + return (_lastSmma * _periodMinusOne + input) * _periodRecip; + } + protected override double Calculation() { ManageState(Input.IsNew); - - _buffer!.Add(Input.Value, Input.IsNew); + _buffer.Add(Input.Value, Input.IsNew); double smma; - if (_index <= _period) { smma = _buffer.Average(); - if (_index == _period) { _lastSmma = smma; // Initialize _lastSmma for the transition @@ -93,7 +103,7 @@ public class Smma : AbstractBase } else { - smma = ((_lastSmma * (_period - 1)) + Input.Value) / _period; + smma = CalculateSmma(Input.Value); } _lastSmma = smma; diff --git a/lib/averages/T3.cs b/lib/averages/T3.cs index 49a00cb5..f0d3402f 100644 --- a/lib/averages/T3.cs +++ b/lib/averages/T3.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -31,8 +31,10 @@ public class T3 : AbstractBase private readonly int _period; private readonly double _vfactor; private readonly bool _useSma; - private readonly double _k, _k1m, _c1, _c2, _c3, _c4; + private readonly double _k; + private readonly double _c1, _c2, _c3, _c4; private readonly CircularBuffer _buffer1, _buffer2, _buffer3, _buffer4, _buffer5, _buffer6; + private double _lastEma1, _lastEma2, _lastEma3, _lastEma4, _lastEma5, _lastEma6; private double _p_lastEma1, _p_lastEma2, _p_lastEma3, _p_lastEma4, _p_lastEma5, _p_lastEma6; @@ -44,7 +46,7 @@ public class T3 : AbstractBase { if (period < 1) { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } _period = period; _vfactor = vfactor; @@ -52,11 +54,14 @@ public class T3 : AbstractBase WarmupPeriod = period; _k = 2.0 / (_period + 1); - _k1m = 1.0 - _k; - _c1 = -_vfactor * _vfactor * _vfactor; - _c2 = 3 * _vfactor * _vfactor + 3 * _vfactor * _vfactor * _vfactor; - _c3 = -6 * _vfactor * _vfactor - 3 * _vfactor - 3 * _vfactor * _vfactor * _vfactor; - _c4 = 1 + 3 * _vfactor + _vfactor * _vfactor * _vfactor + 3 * _vfactor * _vfactor; + + // Precalculate coefficients + double v2 = vfactor * vfactor; + double v3 = v2 * vfactor; + _c1 = -v3; + _c2 = 3.0 * (v2 + v3); + _c3 = -3.0 * (2.0 * v2 + vfactor + v3); + _c4 = 1.0 + 3.0 * vfactor + v3 + 3.0 * v2; _buffer1 = new(period); _buffer2 = new(period); @@ -79,6 +84,7 @@ public class T3 : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { _lastEma1 = _lastEma2 = _lastEma3 = _lastEma4 = _lastEma5 = _lastEma6 = 0; @@ -90,6 +96,7 @@ public class T3 : AbstractBase _buffer6.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -114,6 +121,18 @@ public class T3 : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateEma(double input, double lastEma) + { + return _k * (input - lastEma) + lastEma; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateT3(double ema3, double ema4, double ema5, double ema6) + { + return _c1 * ema6 + _c2 * ema5 + _c3 * ema4 + _c4 * ema3; + } + protected override double Calculation() { ManageState(Input.IsNew); @@ -141,12 +160,12 @@ public class T3 : AbstractBase } else { - ema1 = _k * (Input.Value - _lastEma1) + _lastEma1; - ema2 = _k * (ema1 - _lastEma2) + _lastEma2; - ema3 = _k * (ema2 - _lastEma3) + _lastEma3; - ema4 = _k * (ema3 - _lastEma4) + _lastEma4; - ema5 = _k * (ema4 - _lastEma5) + _lastEma5; - ema6 = _k * (ema5 - _lastEma6) + _lastEma6; + ema1 = CalculateEma(Input.Value, _lastEma1); + ema2 = CalculateEma(ema1, _lastEma2); + ema3 = CalculateEma(ema2, _lastEma3); + ema4 = CalculateEma(ema3, _lastEma4); + ema5 = CalculateEma(ema4, _lastEma5); + ema6 = CalculateEma(ema5, _lastEma6); } _lastEma1 = ema1; @@ -156,9 +175,7 @@ public class T3 : AbstractBase _lastEma5 = ema5; _lastEma6 = ema6; - double t3 = _c1 * ema6 + _c2 * ema5 + _c3 * ema4 + _c4 * ema3; - IsHot = _index >= WarmupPeriod; - return t3; + return CalculateT3(ema3, ema4, ema5, ema6); } } diff --git a/lib/averages/Tema.cs b/lib/averages/Tema.cs index 1090cd2a..ac85723c 100644 --- a/lib/averages/Tema.cs +++ b/lib/averages/Tema.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -29,10 +29,13 @@ namespace QuanTAlib; public class Tema : AbstractBase { private readonly int _period; + private readonly double _k; + private readonly double _oneMinusK; + private readonly double _epsilon = 1e-10; private double _lastEma1, _p_lastEma1; private double _lastEma2, _p_lastEma2; private double _lastEma3, _p_lastEma3; - private double _k, _e, _p_e; + private double _e, _p_e; /// The number of periods used in each EMA calculation. /// Thrown when period is less than 1. @@ -40,12 +43,14 @@ public class Tema : AbstractBase { if (period < 1) { - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); + throw new System.ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); } _period = period; + _k = 2.0 / (_period + 1); + _oneMinusK = 1.0 - _k; Name = "Tema"; double percentile = 0.85; //targeting 85th percentile of correctness of converging EMA - WarmupPeriod = (int)Math.Ceiling(-period * Math.Log(1 - percentile)); + WarmupPeriod = (int)System.Math.Ceiling(-period * System.Math.Log(1 - percentile)); Init(); } @@ -57,14 +62,15 @@ public class Tema : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); - _k = 2.0 / (_period + 1); _e = 1.0; _lastEma1 = _lastEma2 = _lastEma3 = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -84,24 +90,37 @@ public class Tema : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateEma(double input, double lastEma, double invE) + { + return _k * (input * invE - lastEma) + lastEma; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double UpdateCompensator() + { + _e = (_e > _epsilon) ? _oneMinusK * _e : 0; + return (_e > _epsilon) ? 1.0 / (1.0 - _e) : 1.0; + } + protected override double Calculation() { - double result, _ema1, _ema2, _ema3; ManageState(Input.IsNew); - _e = (_e > 1e-10) ? (1 - _k) * _e : 0; - double _invE = (_e > 1e-10) ? 1 / (1 - _e) : 1; + double invE = UpdateCompensator(); - _ema1 = _k * (Input.Value - _lastEma1) + _lastEma1; - _ema2 = _k * (_ema1 * _invE - _lastEma2) + _lastEma2; - _ema3 = _k * (_ema2 * _invE - _lastEma3) + _lastEma3; + // Calculate EMAs with compensation + double ema1 = CalculateEma(Input.Value, _lastEma1, 1.0); // First EMA doesn't need compensation + double ema2 = CalculateEma(ema1, _lastEma2, invE); + double ema3 = CalculateEma(ema2, _lastEma3, invE); - double _tema = 3 * _ema1 * _invE - 3 * _ema2 * _invE + _ema3 * _invE; + // Store values for next iteration + _lastEma1 = ema1; + _lastEma2 = ema2; + _lastEma3 = ema3; - result = _tema; - _lastEma1 = _ema1; - _lastEma2 = _ema2; - _lastEma3 = _ema3; + // Calculate final TEMA with compensation + double result = (3.0 * ema1 - 3.0 * ema2 + ema3) * invE; IsHot = _index >= WarmupPeriod; return result; diff --git a/lib/averages/Trima.cs b/lib/averages/Trima.cs index 93e8a17b..cc4ccb3c 100644 --- a/lib/averages/Trima.cs +++ b/lib/averages/Trima.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -29,6 +29,7 @@ namespace QuanTAlib; public class Trima : AbstractBase { private readonly Convolution _convolution; + private readonly double[] _kernel; /// The number of data points used in the TRIMA calculation. /// Thrown when period is less than 1. @@ -36,9 +37,10 @@ public class Trima : AbstractBase { if (period < 1) { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } - _convolution = new Convolution(GenerateKernel(period)); + _kernel = GenerateKernel(period); + _convolution = new Convolution(_kernel); Name = "Trima"; WarmupPeriod = period; Init(); @@ -57,33 +59,38 @@ public class Trima : AbstractBase /// /// The period for which to generate the kernel. /// An array of normalized triangular weights for the convolution operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double[] GenerateKernel(int period) { double[] kernel = new double[period]; int halfPeriod = (period + 1) / 2; double weightSum = 0; + // Calculate weights and sum in one pass for (int i = 0; i < period; i++) { kernel[i] = i < halfPeriod ? i + 1 : period - i; weightSum += kernel[i]; } - // Normalize the kernel + // Normalize using multiplication instead of division + double invWeightSum = 1.0 / weightSum; for (int i = 0; i < period; i++) { - kernel[i] /= weightSum; + kernel[i] *= invWeightSum; } return kernel; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private new void Init() { base.Init(); _convolution.Init(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -98,11 +105,9 @@ public class Trima : AbstractBase ManageState(Input.IsNew); // Use Convolution for calculation - TValue convolutionResult = _convolution.Calc(Input); - - double result = convolutionResult.Value; + var convolutionResult = _convolution.Calc(Input); IsHot = _index >= WarmupPeriod; - return result; + return convolutionResult.Value; } } diff --git a/lib/averages/Vidya.cs b/lib/averages/Vidya.cs index 125b3e68..1d8b64d3 100644 --- a/lib/averages/Vidya.cs +++ b/lib/averages/Vidya.cs @@ -1,7 +1,4 @@ -using System; -using System.Linq; using System.Runtime.CompilerServices; - namespace QuanTAlib; /// @@ -33,9 +30,9 @@ public class Vidya : AbstractBase { private readonly int _longPeriod; private readonly double _alpha; + private readonly CircularBuffer _shortBuffer; + private readonly CircularBuffer _longBuffer; private double _lastVIDYA, _p_lastVIDYA; - private readonly CircularBuffer? _shortBuffer; - private readonly CircularBuffer? _longBuffer; /// The number of periods for short-term volatility calculation. /// The number of periods for long-term volatility calculation (default is 4x shortPeriod). @@ -45,14 +42,14 @@ public class Vidya : AbstractBase { if (shortPeriod < 1) { - throw new ArgumentException("Short period must be greater than or equal to 1.", nameof(shortPeriod)); + throw new System.ArgumentException("Short period must be greater than or equal to 1.", nameof(shortPeriod)); } _longPeriod = (longPeriod == 0) ? shortPeriod * 4 : longPeriod; _alpha = alpha; - WarmupPeriod = _longPeriod; - Name = $"Vidya({shortPeriod},{_longPeriod})"; _shortBuffer = new CircularBuffer(shortPeriod); _longBuffer = new CircularBuffer(_longPeriod); + WarmupPeriod = _longPeriod; + Name = $"Vidya({shortPeriod},{_longPeriod})"; Init(); } @@ -67,12 +64,14 @@ public class Vidya : AbstractBase pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _lastVIDYA = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -87,12 +86,35 @@ public class Vidya : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double CalculateStdDev(CircularBuffer buffer) + { + double mean = buffer.Average(); + double sumSquaredDiff = 0; + var span = buffer.GetSpan(); + + for (int i = 0; i < buffer.Count; i++) + { + double diff = span[i] - mean; + sumSquaredDiff += diff * diff; + } + + return System.Math.Sqrt(sumSquaredDiff / buffer.Count); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateVidya(double shortStdDev, double longStdDev) + { + double s = _alpha * (shortStdDev / longStdDev); + return (s * Input.Value) + ((1.0 - s) * _lastVIDYA); + } + protected override double Calculation() { ManageState(Input.IsNew); - _shortBuffer!.Add(Input.Value, Input.IsNew); - _longBuffer!.Add(Input.Value, Input.IsNew); + _shortBuffer.Add(Input.Value, Input.IsNew); + _longBuffer.Add(Input.Value, Input.IsNew); double vidya; if (_index <= _longPeriod) @@ -103,8 +125,7 @@ public class Vidya : AbstractBase { double shortStdDev = CalculateStdDev(_shortBuffer); double longStdDev = CalculateStdDev(_longBuffer); - double s = _alpha * (shortStdDev / longStdDev); - vidya = (s * Input.Value) + ((1 - s) * _lastVIDYA); + vidya = CalculateVidya(shortStdDev, longStdDev); } _lastVIDYA = vidya; @@ -112,17 +133,4 @@ public class Vidya : AbstractBase return vidya; } - - /// - /// Calculates the standard deviation of values in a circular buffer. - /// - /// The circular buffer containing the values. - /// The standard deviation of the values in the buffer. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static double CalculateStdDev(CircularBuffer buffer) - { - double mean = buffer.Average(); - double sumSquaredDiff = buffer.Sum(x => Math.Pow(x - mean, 2)); - return Math.Sqrt(sumSquaredDiff / buffer.Count); - } } diff --git a/lib/averages/Wma.cs b/lib/averages/Wma.cs index b3ae1167..ce855a83 100644 --- a/lib/averages/Wma.cs +++ b/lib/averages/Wma.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -31,6 +31,7 @@ public class Wma : AbstractBase { private readonly int _period; private readonly Convolution _convolution; + private readonly double[] _kernel; /// The number of data points used in the WMA calculation. /// Thrown when period is less than 1. @@ -38,10 +39,11 @@ public class Wma : AbstractBase { if (period < 1) { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } _period = period; - _convolution = new Convolution(GenerateWmaKernel(_period)); + _kernel = GenerateWmaKernel(_period); + _convolution = new Convolution(_kernel); Name = "Wma"; WarmupPeriod = _period; Init(); @@ -60,25 +62,29 @@ public class Wma : AbstractBase /// /// The period for which to generate the kernel. /// An array of normalized linearly decreasing weights for the convolution operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double[] GenerateWmaKernel(int period) { double[] kernel = new double[period]; - double weightSum = period * (period + 1) / 2.0; + double weightSum = period * (period + 1) * 0.5; // Multiply by 0.5 instead of dividing by 2 + double invWeightSum = 1.0 / weightSum; for (int i = 0; i < period; i++) { - kernel[i] = (period - i) / weightSum; + kernel[i] = (period - i) * invWeightSum; } return kernel; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private new void Init() { base.Init(); _convolution.Init(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -93,11 +99,9 @@ public class Wma : AbstractBase ManageState(Input.IsNew); // Use Convolution for calculation - TValue convolutionResult = _convolution.Calc(Input); - - double result = convolutionResult.Value; + var convolutionResult = _convolution.Calc(Input); IsHot = _index >= WarmupPeriod; - return result; + return convolutionResult.Value; } } diff --git a/lib/averages/Zlema.cs b/lib/averages/Zlema.cs index b88374b1..cb339950 100644 --- a/lib/averages/Zlema.cs +++ b/lib/averages/Zlema.cs @@ -1,104 +1,113 @@ -using System; using System.Runtime.CompilerServices; +namespace QuanTAlib; -namespace QuanTAlib +/// +/// ZLEMA: Zero Lag Exponential Moving Average +/// A modified exponential moving average designed to reduce lag by incorporating +/// error correction based on predicted values. It estimates and removes lag by +/// extrapolating the trend using the difference between current and lagged prices. +/// +/// +/// The ZLEMA calculation process: +/// 1. Calculates lag period as (period - 1) / 2 +/// 2. Gets error correction term: 2 * price - lag_price +/// 3. Applies EMA to error-corrected price +/// 4. Results in reduced lag compared to standard EMA +/// +/// Key characteristics: +/// - Significantly reduced lag compared to EMA +/// - More responsive to price changes +/// - Uses error correction mechanism +/// - Maintains smoothness despite reduced lag +/// - Better trend following capabilities +/// +/// Sources: +/// John Ehlers and Ric Way - "Zero Lag (Well, Almost)" +/// Technical Analysis of Stocks and Commodities, 2010 +/// + +public class Zlema : AbstractBase { - /// - /// ZLEMA: Zero Lag Exponential Moving Average - /// A modified exponential moving average designed to reduce lag by incorporating - /// error correction based on predicted values. It estimates and removes lag by - /// extrapolating the trend using the difference between current and lagged prices. - /// - /// - /// The ZLEMA calculation process: - /// 1. Calculates lag period as (period - 1) / 2 - /// 2. Gets error correction term: 2 * price - lag_price - /// 3. Applies EMA to error-corrected price - /// 4. Results in reduced lag compared to standard EMA - /// - /// Key characteristics: - /// - Significantly reduced lag compared to EMA - /// - More responsive to price changes - /// - Uses error correction mechanism - /// - Maintains smoothness despite reduced lag - /// - Better trend following capabilities - /// - /// Sources: - /// John Ehlers and Ric Way - "Zero Lag (Well, Almost)" - /// Technical Analysis of Stocks and Commodities, 2010 - /// + private readonly CircularBuffer _buffer; + private readonly int _lag; + private readonly Ema _ema; + private double _lastZLEMA, _p_lastZLEMA; - public class Zlema : AbstractBase + /// The number of periods used in the ZLEMA calculation. + /// Thrown when period is less than 1. + public Zlema(int period) { - private readonly CircularBuffer _buffer; - private readonly int _lag; - private readonly Ema _ema; - private double _lastZLEMA, _p_lastZLEMA; - - /// The number of periods used in the ZLEMA calculation. - /// Thrown when period is less than 1. - public Zlema(int period) + if (period < 1) { - if (period < 1) - { - throw new ArgumentException("Period must be greater than or equal to 1.", nameof(period)); - } - WarmupPeriod = period; - _lag = (int)(0.5 * (period - 1)); - _buffer = new CircularBuffer(_lag + 1); - _ema = new Ema(period, useSma: false); - Name = $"Zlema({period})"; - Init(); + throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } + WarmupPeriod = period; + _lag = (int)(0.5 * (period - 1)); + _buffer = new CircularBuffer(_lag + 1); + _ema = new Ema(period, useSma: false); + Name = $"Zlema({period})"; + Init(); + } - /// The data source object that publishes updates. - /// The number of periods used in the ZLEMA calculation. - public Zlema(object source, int period) : this(period) + /// The data source object that publishes updates. + /// The number of periods used in the ZLEMA calculation. + public Zlema(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _buffer.Clear(); + _ema.Init(); + _lastZLEMA = 0; + _p_lastZLEMA = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) { - var pubEvent = source.GetType().GetEvent("Pub"); - pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + _lastValidValue = Input.Value; + _index++; + _p_lastZLEMA = _lastZLEMA; } - - public override void Init() + else { - base.Init(); - _buffer.Clear(); - _ema.Init(); - _lastZLEMA = 0; - _p_lastZLEMA = 0; - } - - protected override void ManageState(bool isNew) - { - if (isNew) - { - _lastValidValue = Input.Value; - _index++; - _p_lastZLEMA = _lastZLEMA; - } - else - { - _lastZLEMA = _p_lastZLEMA; - } - } - - protected override double Calculation() - { - ManageState(Input.IsNew); - - _buffer.Add(Input.Value, Input.IsNew); - - // Get lagged value and calculate error correction - double lagValue = _buffer[Math.Max(0, _buffer.Count - 1 - _lag)]; - double errorCorrection = 2 * Input.Value - lagValue; - - // Apply EMA to error-corrected value - double zlema = _ema.Calc(new TValue(errorCorrection, Input.IsNew)).Value; - - _lastZLEMA = zlema; - IsHot = _index >= WarmupPeriod; - - return zlema; + _lastZLEMA = _p_lastZLEMA; } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateErrorCorrection() + { + double lagValue = _buffer[System.Math.Max(0, _buffer.Count - 1 - _lag)]; + return 2.0 * Input.Value - lagValue; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double CalculateZlema(double errorCorrection) + { + var tempValue = new TValue(Input.Time, errorCorrection, Input.IsNew); + return _ema.Calc(tempValue).Value; + } + + protected override double Calculation() + { + ManageState(Input.IsNew); + _buffer.Add(Input.Value, Input.IsNew); + + // Calculate error correction and apply EMA + double errorCorrection = CalculateErrorCorrection(); + double zlema = CalculateZlema(errorCorrection); + + _lastZLEMA = zlema; + IsHot = _index >= WarmupPeriod; + + return zlema; + } } diff --git a/lib/core/AbstractBarBase.cs b/lib/core/AbstractBarBase.cs index 3e46d8d9..1cbaf47f 100644 --- a/lib/core/AbstractBarBase.cs +++ b/lib/core/AbstractBarBase.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -10,17 +11,21 @@ namespace QuanTAlib; /// public abstract class AbstractBarBase : ITValue { - public DateTime Time { get; set; } + public System.DateTime Time { get; set; } public double Value { get; set; } public bool IsNew { get; set; } public bool IsHot { get; set; } public TBar Input { get; set; } - public String Name { get; set; } = ""; + public string Name { get; set; } = ""; public int WarmupPeriod { get; set; } + public TValue Tick => new(Time, Value, IsNew, IsHot); + public event ValueSignal Pub = delegate { }; + protected int _index; protected double _lastValidValue; + protected AbstractBarBase() { // Add parameters into constructor if needed @@ -31,17 +36,41 @@ public abstract class AbstractBarBase : ITValue /// /// The source of the bar data. /// The event arguments containing the bar data. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Sub(object source, in TBarEventArgs args) => Calc(args.Bar); /// /// Initializes the indicator's state. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual void Init() { _index = 0; _lastValidValue = 0; } + /// + /// Checks if the input value is valid (not NaN or Infinity). + /// + /// The value to check. + /// True if the value is valid, false otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected static bool IsValidValue(double value) + { + return !double.IsNaN(value) && !double.IsInfinity(value); + } + + /// + /// Creates a new TValue with the current state. + /// + /// The value to use. + /// A new TValue instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected TValue CreateTValue(double value) + { + return new TValue(Time: Input.Time, Value: value, IsNew: Input.IsNew, IsHot: IsHot); + } + /// /// Calculates the indicator value based on the input bar. /// @@ -50,21 +79,23 @@ public abstract class AbstractBarBase : ITValue public virtual TValue Calc(TBar input) { Input = input; - if (double.IsNaN(input.Close) || double.IsInfinity(input.Close)) + if (!IsValidValue(input.Close)) { - return Process(new TValue(Time: input.Time, Value: GetLastValid(), IsNew: input.IsNew, IsHot: true)); + return Process(CreateTValue(GetLastValid())); } - this.Value = Calculation(); - return Process(new TValue(Time: Input.Time, Value: this.Value, IsNew: Input.IsNew, IsHot: this.IsHot)); + + Value = Calculation(); + return Process(CreateTValue(Value)); } /// /// Retrieves the last valid calculated value. /// /// The last valid value of the indicator. + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected virtual double GetLastValid() { - return this.Value; + return Value; } /// @@ -85,12 +116,13 @@ public abstract class AbstractBarBase : ITValue /// /// The calculated TValue to process. /// The processed TValue. + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected virtual TValue Process(TValue value) { - this.Time = value.Time; - this.Value = value.Value; - this.IsNew = value.IsNew; - this.IsHot = value.IsHot; + Time = value.Time; + Value = value.Value; + IsNew = value.IsNew; + IsHot = value.IsHot; Pub?.Invoke(this, new ValueEventArgs(value)); return value; } diff --git a/lib/core/abstractBase.cs b/lib/core/abstractBase.cs index 3ac3d973..2deb5fe7 100644 --- a/lib/core/abstractBase.cs +++ b/lib/core/abstractBase.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -10,7 +11,7 @@ namespace QuanTAlib; /// public abstract class AbstractBase : ITValue { - public DateTime Time { get; set; } + public System.DateTime Time { get; set; } public double Value { get; set; } public bool IsNew { get; set; } public bool IsHot { get; set; } @@ -18,7 +19,7 @@ public abstract class AbstractBase : ITValue public TValue Input2 { get; set; } public TBar BarInput { get; set; } public TBar BarInput2 { get; set; } - public String Name { get; set; } = ""; + public string Name { get; set; } = ""; public int WarmupPeriod { get; set; } public TValue Tick => new(Time, Value, IsNew, IsHot); public event ValueSignal Pub = delegate { }; @@ -31,45 +32,64 @@ public abstract class AbstractBase : ITValue } /// - /// Subscribes to a data source and triggers calculations on new data. + /// Checks if the input value is valid (not NaN or Infinity). /// - /// The class publishing the data. - /// The argument containing the new data point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected static bool IsValidValue(double value) + { + return !double.IsNaN(value) && !double.IsInfinity(value); + } + + /// + /// Creates a new TValue with the current state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected TValue CreateTValue(System.DateTime time, double value, bool isNew, bool isHot = false) + { + return new TValue(time, value, isNew, isHot); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Sub(object source, in ValueEventArgs args) => Calc(args.Tick); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Sub(object source1, object source2, in ValueEventArgs args1, in ValueEventArgs args2) => Calc(args1.Tick, args2.Tick); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Sub(object source, in TBarEventArgs args) => Calc(args.Bar); - /// - /// Initializes the indicator's state. - /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual void Init() { _index = 0; _lastValidValue = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual TValue Calc(TValue input) { Input = input; - Input2 = new(Time: Input.Time, Value: double.NaN, IsNew: Input.IsNew, IsHot: Input.IsHot); + Input2 = CreateTValue(input.Time, double.NaN, input.IsNew, input.IsHot); return Process(input.Value, input.Time, input.IsNew); } - public virtual TValue Calc(double value, bool IsNew) + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public virtual TValue Calc(double value, bool isNew) { - Input = new(this.Time, Value: value, IsNew: IsNew, IsHot: false); - Input2 = new(this.Time, double.NaN, false, false); + Input = CreateTValue(Time, value, isNew); + Input2 = CreateTValue(Time, double.NaN, false); return Process(Input.Value, Input.Time, Input.IsNew); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual TValue Calc(TBar barInput) { BarInput = barInput; return Process(barInput.Close, barInput.Time, barInput.IsNew); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual TValue Calc(TValue input1, TValue input2) { Input = input1; @@ -77,6 +97,7 @@ public abstract class AbstractBase : ITValue return Process(input1.Value, input2.Value, input1.Time, input1.IsNew); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual TValue Calc(TBar input1, TBar input2) { BarInput = input1; @@ -84,84 +105,55 @@ public abstract class AbstractBase : ITValue return Process(input1.Close, input2.Close, input1.Time, input1.IsNew); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual TValue Calc(double value1, double value2) { - DateTime now = DateTime.Now; - Input = new TValue(now, value1, true, true); - Input2 = new TValue(now, value2, true, true); + var now = System.DateTime.Now; + Input = CreateTValue(now, value1, true, true); + Input2 = CreateTValue(now, value2, true, true); return Process(value1, value2, now, true); } - /// - /// Processes the input values, performs error checking, and calculates the indicator value. - /// - /// The primary input value to process. - /// The timestamp of the input. - /// Indicates if the input is new. - /// A TValue object with the calculated or last valid value. - protected virtual TValue Process(double value, DateTime time, bool isNew) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected virtual TValue Process(double value, System.DateTime time, bool isNew) { - if (double.IsNaN(value) || double.IsInfinity(value)) + if (!IsValidValue(value)) { - return Process(new TValue(time, GetLastValid(), isNew, this.IsHot)); + return Process(CreateTValue(time, GetLastValid(), isNew, IsHot)); } - this.Value = Calculation(); - return Process(new TValue(Time: time, Value: this.Value, IsNew: isNew, IsHot: this.IsHot)); + Value = Calculation(); + return Process(CreateTValue(time, Value, isNew, IsHot)); } - /// - /// Processes two input values, performs error checking, and calculates the indicator value. - /// - /// The first input value to process. - /// The second input value to process. - /// The timestamp of the input. - /// Indicates if the input is new. - /// A TValue object with the calculated or last valid value. - protected virtual TValue Process(double value1, double value2, DateTime time, bool isNew) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected virtual TValue Process(double value1, double value2, System.DateTime time, bool isNew) { - if (double.IsNaN(value1) || double.IsInfinity(value1) || - double.IsNaN(value2) || double.IsInfinity(value2)) + if (!IsValidValue(value1) || !IsValidValue(value2)) { - return Process(new TValue(time, GetLastValid(), isNew, this.IsHot)); + return Process(CreateTValue(time, GetLastValid(), isNew, IsHot)); } - this.Value = Calculation(); - return Process(new TValue(Time: time, Value: this.Value, IsNew: isNew, IsHot: this.IsHot)); + Value = Calculation(); + return Process(CreateTValue(time, Value, isNew, IsHot)); } - /// - /// Processes the calculated value, updates the indicator's own state, - /// and publishes the result through an event. - /// - /// The calculated TValue to process. - /// The processed TValue. + + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected virtual TValue Process(TValue value) { - this.Time = value.Time; - this.Value = value.Value; - this.IsNew = value.IsNew; - this.IsHot = value.IsHot; + Time = value.Time; + Value = value.Value; + IsNew = value.IsNew; + IsHot = value.IsHot; Pub?.Invoke(this, new ValueEventArgs(value)); return value; } - /// - /// Retrieves the last valid calculated value. - /// - /// The last valid value of the indicator. + + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected virtual double GetLastValid() { - return this.Value; + return Value; } - /// - /// Manages the state of the indicator based on whether a new data point is being processed. - /// - /// Indicates whether the current input is a new data point. protected abstract void ManageState(bool isNew); - /// - /// Performs the actual calculation of the indicator value. - /// - /// The calculated indicator value. protected abstract double Calculation(); - - } diff --git a/lib/core/circularbuffer.cs b/lib/core/circularbuffer.cs index 42740f67..a203a8e9 100644 --- a/lib/core/circularbuffer.cs +++ b/lib/core/circularbuffer.cs @@ -12,16 +12,18 @@ namespace QuanTAlib; /// a fixed-size buffer of double values. It uses SIMD operations for improved performance /// on supported hardware. /// +[SkipLocalsInit] public class CircularBuffer : IEnumerable { private readonly double[] _buffer; + private readonly int _capacity; private int _start = 0; private int _size = 0; /// /// Gets the maximum number of elements that can be contained in the buffer. /// - public int Capacity { get; } + public int Capacity => _capacity; /// /// Gets the number of elements currently contained in the buffer. @@ -32,9 +34,10 @@ public class CircularBuffer : IEnumerable /// Initializes a new instance of the CircularBuffer class with the specified capacity. /// /// The maximum number of elements the buffer can hold. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public CircularBuffer(int capacity) { - Capacity = capacity; + _capacity = capacity; _buffer = GC.AllocateArray(capacity, pinned: true); } @@ -48,20 +51,20 @@ public class CircularBuffer : IEnumerable { if (_size == 0 || isNew) { - if (_size < Capacity) + if (_size < _capacity) { - _buffer[(_start + _size) % Capacity] = item; + _buffer[(_start + _size) % _capacity] = item; _size++; } else { _buffer[_start] = item; - _start = (_start + 1) % Capacity; + _start = (_start + 1) % _capacity; } } else { - _buffer[(_start + _size - 1) % Capacity] = item; + _buffer[(_start + _size - 1) % _capacity] = item; } } @@ -77,14 +80,14 @@ public class CircularBuffer : IEnumerable { int actualIndex = index.IsFromEnd ? _size - index.Value : index.Value; actualIndex = Math.Clamp(actualIndex, 0, _size - 1); - return _buffer[(_start + actualIndex) % Capacity]; + return _buffer[(_start + actualIndex) % _capacity]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] set { int actualIndex = index.IsFromEnd ? _size - index.Value : index.Value; actualIndex = Math.Clamp(actualIndex, 0, _size - 1); - _buffer[(_start + actualIndex) % Capacity] = value; + _buffer[(_start + actualIndex) % _capacity] = value; } } @@ -103,7 +106,7 @@ public class CircularBuffer : IEnumerable { if (_size == 0) return 0; - return _buffer[(_start + _size - 1) % Capacity]; + return _buffer[(_start + _size - 1) % _capacity]; } /// @@ -128,6 +131,7 @@ public class CircularBuffer : IEnumerable /// Returns an enumerator that iterates through the buffer. /// /// An enumerator for the buffer. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Enumerator GetEnumerator() => new(this); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); @@ -135,16 +139,18 @@ public class CircularBuffer : IEnumerable /// /// Represents an enumerator for the CircularBuffer. /// - public struct Enumerator : IEnumerator + public readonly struct Enumerator : IEnumerator { private readonly CircularBuffer _buffer; - private int _index; - private double _current; + private readonly int _size; + private readonly int _index; + private readonly double _current; [MethodImpl(MethodImplOptions.AggressiveInlining)] internal Enumerator(CircularBuffer buffer) { _buffer = buffer; + _size = buffer._size; _index = -1; _current = default; } @@ -156,11 +162,11 @@ public class CircularBuffer : IEnumerable [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool MoveNext() { - if (_index + 1 >= _buffer._size) + if (_index + 1 >= _size) return false; - _index++; - _current = _buffer[_index]; + Unsafe.AsRef(in _index)++; + Unsafe.AsRef(in _current) = _buffer[_index]; return true; } @@ -173,10 +179,11 @@ public class CircularBuffer : IEnumerable /// /// Sets the enumerator to its initial position, which is before the first element in the buffer. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { - _index = -1; - _current = default; + Unsafe.AsRef(in _index) = -1; + Unsafe.AsRef(in _current) = default; } /// @@ -196,13 +203,13 @@ public class CircularBuffer : IEnumerable if (_size == 0) return; - if (_start + _size <= Capacity) + if (_start + _size <= _capacity) { Array.Copy(_buffer, _start, destination, destinationIndex, _size); } else { - int firstPartLength = Capacity - _start; + int firstPartLength = _capacity - _start; Array.Copy(_buffer, _start, destination, destinationIndex, firstPartLength); Array.Copy(_buffer, 0, destination, destinationIndex + firstPartLength, _size - firstPartLength); } @@ -218,7 +225,7 @@ public class CircularBuffer : IEnumerable if (_size == 0) return ReadOnlySpan.Empty; - if (_start + _size <= Capacity) + if (_start + _size <= _capacity) { return new ReadOnlySpan(_buffer, _start, _size); } @@ -298,7 +305,7 @@ public class CircularBuffer : IEnumerable return SumSimd() / _size; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private double MaxSimd() { var span = GetSpan(); @@ -306,9 +313,11 @@ public class CircularBuffer : IEnumerable var maxVector = new Vector(double.MinValue); int i = 0; + ref double spanRef = ref System.Runtime.InteropServices.MemoryMarshal.GetReference(span); + for (; i <= span.Length - vectorSize; i += vectorSize) { - maxVector = Vector.Max(maxVector, new Vector(span.Slice(i, vectorSize))); + maxVector = Vector.Max(maxVector, Unsafe.As>(ref Unsafe.Add(ref spanRef, i))); } double max = double.MinValue; @@ -325,7 +334,7 @@ public class CircularBuffer : IEnumerable return max; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private double MinSimd() { var span = GetSpan(); @@ -333,9 +342,11 @@ public class CircularBuffer : IEnumerable var minVector = new Vector(double.MaxValue); int i = 0; + ref double spanRef = ref System.Runtime.InteropServices.MemoryMarshal.GetReference(span); + for (; i <= span.Length - vectorSize; i += vectorSize) { - minVector = Vector.Min(minVector, new Vector(span.Slice(i, vectorSize))); + minVector = Vector.Min(minVector, Unsafe.As>(ref Unsafe.Add(ref spanRef, i))); } double min = double.MaxValue; @@ -352,7 +363,7 @@ public class CircularBuffer : IEnumerable return min; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private double SumSimd() { var span = GetSpan(); @@ -360,9 +371,11 @@ public class CircularBuffer : IEnumerable var sumVector = Vector.Zero; int i = 0; + ref double spanRef = ref System.Runtime.InteropServices.MemoryMarshal.GetReference(span); + for (; i <= span.Length - vectorSize; i += vectorSize) { - sumVector += new Vector(span.Slice(i, vectorSize)); + sumVector += Unsafe.As>(ref Unsafe.Add(ref spanRef, i)); } double sum = 0; @@ -383,6 +396,7 @@ public class CircularBuffer : IEnumerable /// Copies the buffer elements to a new array. /// /// An array containing copies of the buffer elements. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public double[] ToArray() { double[] array = new double[_size]; @@ -394,6 +408,7 @@ public class CircularBuffer : IEnumerable /// Performs a parallel operation on the buffer elements. /// /// The operation to perform on each partition of the buffer. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ParallelOperation(Func operation) { const int MinimumPartitionSize = 1024; @@ -416,7 +431,7 @@ public class CircularBuffer : IEnumerable } var buffer = ToArray(); - var results = new double[partitionCount]; + var results = GC.AllocateUninitializedArray(partitionCount); Parallel.For(0, partitionCount, i => { @@ -425,4 +440,4 @@ public class CircularBuffer : IEnumerable results[i] = operation(buffer, start, length); }); } -} \ No newline at end of file +} diff --git a/lib/core/tbar.cs b/lib/core/tbar.cs index 9b72bc88..c27682f4 100644 --- a/lib/core/tbar.cs +++ b/lib/core/tbar.cs @@ -1,3 +1,5 @@ +using System.Runtime.CompilerServices; + namespace QuanTAlib; public interface ITBar @@ -11,53 +13,74 @@ public interface ITBar bool IsNew { get; } } +[SkipLocalsInit] public readonly record struct TBar(DateTime Time, double Open, double High, double Low, double Close, double Volume, bool IsNew = true) : ITBar { public DateTime Time { get; init; } = Time; -public double Open { get; init; } = Open; -public double High { get; init; } = High; -public double Low { get; init; } = Low; -public double Close { get; init; } = Close; -public double Volume { get; init; } = Volume; -public bool IsNew { get; init; } = IsNew; + public double Open { get; init; } = Open; + public double High { get; init; } = High; + public double Low { get; init; } = Low; + public double Close { get; init; } = Close; + public double Volume { get; init; } = Volume; + public bool IsNew { get; init; } = IsNew; -public double HL2 => (High + Low) * 0.5; -public double OC2 => (Open + Close) * 0.5; -public double OHL3 => (Open + High + Low) / 3; -public double HLC3 => (High + Low + Close) / 3; -public double OHLC4 => (Open + High + Low + Close) * 0.25; -public double HLCC4 => (High + Low + Close + Close) * 0.25; + public double HL2 => (High + Low) * 0.5; + public double OC2 => (Open + Close) * 0.5; + public double OHL3 => (Open + High + Low) / 3; + public double HLC3 => (High + Low + Close) / 3; + public double OHLC4 => (Open + High + Low + Close) * 0.25; + public double HLCC4 => (High + Low + Close + Close) * 0.25; -public TBar() : this(DateTime.UtcNow, 0, 0, 0, 0, 0) { } -public TBar(double Open, double High, double Low, double Close, double Volume, bool IsNew = true) : this(DateTime.UtcNow, Open, High, Low, Close, Volume, IsNew) { } -public TBar(double value) : this(Time: DateTime.UtcNow, Open: value, High: value, Low: value, Close: value, Volume: value, IsNew: true) { } -public TBar(TValue value) : this(Time: value.Time, Open: value.Value, High: value.Value, Low: value.Value, Close: value.Value, Volume: value.Value, IsNew: value.IsNew) { } -public TBar(TBar v) : this(Time: v.Time, Open: v.Open, High: v.High, Low: v.Low, Close: v.Close, Volume: v.Volume, IsNew: true) { } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TBar() : this(DateTime.UtcNow, 0, 0, 0, 0, 0) { } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TBar(double Open, double High, double Low, double Close, double Volume, bool IsNew = true) + : this(DateTime.UtcNow, Open, High, Low, Close, Volume, IsNew) { } -public static implicit operator double(TBar bar) => bar.Close; -public static implicit operator DateTime(TBar tv) => tv.Time; -public override string ToString() => $"[{Time:yyyy-MM-dd HH:mm:ss}: O={Open:F2}, H={High:F2}, L={Low:F2}, C={Close:F2}, V={Volume:F2}]"; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TBar(double value) + : this(Time: DateTime.UtcNow, Open: value, High: value, Low: value, Close: value, Volume: value, IsNew: true) { } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TBar(TValue value) + : this(Time: value.Time, Open: value.Value, High: value.Value, Low: value.Value, Close: value.Value, Volume: value.Value, IsNew: value.IsNew) { } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TBar(TBar v) + : this(Time: v.Time, Open: v.Open, High: v.High, Low: v.Low, Close: v.Close, Volume: v.Volume, IsNew: true) { } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator double(TBar bar) => bar.Close; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator DateTime(TBar tv) => tv.Time; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override string ToString() => $"[{Time:yyyy-MM-dd HH:mm:ss}: O={Open:F2}, H={High:F2}, L={Low:F2}, C={Close:F2}, V={Volume:F2}]"; } public delegate void BarSignal(object source, in TBarEventArgs args); -public class TBarEventArgs : EventArgs +[SkipLocalsInit] +public sealed class TBarEventArgs : EventArgs { - public TBar Bar { get; } - public TBarEventArgs(TBar bar) { Bar = bar; } + public readonly TBar Bar; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TBarEventArgs(TBar bar) => Bar = bar; } +[SkipLocalsInit] public class TBarSeries : List { - private readonly TBar Default = new(DateTime.MinValue, double.NaN, double.NaN, double.NaN, double.NaN, double.NaN); - - public TSeries Open; - public TSeries High; - public TSeries Low; - public TSeries Close; - public TSeries Volume; + private static readonly TBar Default = new(DateTime.MinValue, double.NaN, double.NaN, double.NaN, double.NaN, double.NaN); + public readonly TSeries Open; + public readonly TSeries High; + public readonly TSeries Low; + public readonly TSeries Close; + public readonly TSeries Volume; public TBar Last => Count > 0 ? this[^1] : Default; public TBar First => Count > 0 ? this[0] : Default; @@ -65,22 +88,36 @@ public class TBarSeries : List public string Name { get; set; } public event BarSignal Pub = delegate { }; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public TBarSeries() { - this.Name = "Bar"; - (Open, High, Low, Close, Volume) = ([], [], [], [], []); - + Name = "Bar"; + Open = new TSeries(); + High = new TSeries(); + Low = new TSeries(); + Close = new TSeries(); + Volume = new TSeries(); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public TBarSeries(object source) : this() { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new BarSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public new virtual void Add(TBar bar) { - if (bar.IsNew || base.Count == 0) { base.Add(bar); } - else { this[^1] = bar; } + if (bar.IsNew || base.Count == 0) + { + base.Add(bar); + } + else + { + this[^1] = bar; + } + Pub?.Invoke(this, new TBarEventArgs(bar)); Open.Add(bar.Time, bar.Open, IsNew: bar.IsNew, IsHot: true); @@ -89,18 +126,22 @@ public class TBarSeries : List Close.Add(bar.Time, bar.Close, IsNew: bar.IsNew, IsHot: true); Volume.Add(bar.Time, bar.Volume, IsNew: bar.IsNew, IsHot: true); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(DateTime Time, double Open, double High, double Low, double Close, double Volume, bool IsNew = true) => - this.Add(new TBar(Time, Open, High, Low, Close, Volume, IsNew)); + Add(new TBar(Time, Open, High, Low, Close, Volume, IsNew)); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(double Open, double High, double Low, double Close, double Volume, bool IsNew = true) => - this.Add(new TBar(DateTime.Now, Open, High, Low, Close, Volume, IsNew)); + Add(new TBar(DateTime.Now, Open, High, Low, Close, Volume, IsNew)); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(TBarSeries series) { if (series == this) { // If adding itself, create a copy to avoid modification during enumeration - var copy = new TBarSeries { Name = this.Name }; + var copy = new TBarSeries { Name = Name }; copy.AddRange(this); AddRange(copy); } @@ -109,6 +150,8 @@ public class TBarSeries : List AddRange(series); } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public new virtual void AddRange(IEnumerable collection) { foreach (var item in collection) @@ -117,8 +160,9 @@ public class TBarSeries : List } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Sub(object source, in TBarEventArgs args) { Add(args.Bar); } -} \ No newline at end of file +} diff --git a/lib/core/tvalue.cs b/lib/core/tvalue.cs index 843768d6..8e54ba48 100644 --- a/lib/core/tvalue.cs +++ b/lib/core/tvalue.cs @@ -1,3 +1,5 @@ +using System.Runtime.CompilerServices; + namespace QuanTAlib; public interface ITValue @@ -8,35 +10,52 @@ public interface ITValue bool IsHot { get; } } +[SkipLocalsInit] public readonly record struct TValue(DateTime Time, double Value, bool IsNew = true, bool IsHot = true) : ITValue { public DateTime Time { get; init; } = Time; -public double Value { get; init; } = Value; -public bool IsNew { get; init; } = IsNew; -public bool IsHot { get; init; } = IsHot; -public DateTime t => Time; -public double v => Value; + public double Value { get; init; } = Value; + public bool IsNew { get; init; } = IsNew; + public bool IsHot { get; init; } = IsHot; + public DateTime t => Time; + public double v => Value; -public TValue() : this(DateTime.UtcNow, 0) { } -public TValue(double value, bool isNew = true, bool isHot = true) : this(DateTime.UtcNow, value, IsNew: isNew, IsHot: isHot) { } -public static implicit operator double(TValue tv) => tv.Value; -public static implicit operator DateTime(TValue tv) => tv.Time; -public static implicit operator TValue(double value) => new TValue(DateTime.UtcNow, value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue() : this(DateTime.UtcNow, 0) { } -public override string ToString() => $"[{Time:yyyy-MM-dd HH:mm:ss}, {Value:F2}, IsNew: {IsNew}, IsHot: {IsHot}]"; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue(double value, bool isNew = true, bool isHot = true) + : this(DateTime.UtcNow, value, IsNew: isNew, IsHot: isHot) { } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator double(TValue tv) => tv.Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator DateTime(TValue tv) => tv.Time; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator TValue(double value) => new TValue(DateTime.UtcNow, value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override string ToString() => $"[{Time:yyyy-MM-dd HH:mm:ss}, {Value:F2}, IsNew: {IsNew}, IsHot: {IsHot}]"; } public delegate void ValueSignal(object source, in ValueEventArgs args); -public class ValueEventArgs : EventArgs +[SkipLocalsInit] +public sealed class ValueEventArgs : EventArgs { - public TValue Tick { get; } - public ValueEventArgs(TValue value) { Tick = value; } + public readonly TValue Tick; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ValueEventArgs(TValue value) => Tick = value; } +[SkipLocalsInit] public class TSeries : List { - private readonly TValue Default = new(DateTime.MinValue, double.NaN); + private static readonly TValue Default = new(DateTime.MinValue, double.NaN); + public IEnumerable t => this.Select(item => item.t); public IEnumerable v => this.Select(item => item.v); public TValue Last => Count > 0 ? this[^1] : Default; @@ -45,35 +64,51 @@ public class TSeries : List public string Name { get; set; } public event ValueSignal Pub = delegate { }; - public TSeries() { this.Name = "Data"; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TSeries() + { + Name = "Data"; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public TSeries(object source) : this() { var pubEvent = source.GetType().GetEvent("Pub"); if (pubEvent != null) { -/* - var nameProperty = source.GetType().GetProperty("Name"); - if (nameProperty != null) - { - Name = nameProperty.GetValue(nameProperty)?.ToString()!; - } -*/ pubEvent.AddEventHandler(source, new ValueSignal(Sub)); } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static explicit operator List(TSeries series) => series.Select(item => item.Value).ToList(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static explicit operator double[](TSeries series) => series.Select(item => item.Value).ToArray(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public new virtual void Add(TValue tick) { - if (tick.IsNew || base.Count == 0) { base.Add(tick); } - else { this[^1] = tick; } + if (tick.IsNew || base.Count == 0) + { + base.Add(tick); + } + else + { + this[^1] = tick; + } Pub?.Invoke(this, new ValueEventArgs(tick)); } - public virtual void Add(DateTime Time, double Value, bool IsNew = true, bool IsHot = true) => this.Add(new TValue(Time, Value, IsNew, IsHot)); - public virtual void Add(double Value, bool IsNew = true, bool IsHot = true) => this.Add(new TValue(DateTime.UtcNow, Value, IsNew, IsHot)); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public virtual void Add(DateTime Time, double Value, bool IsNew = true, bool IsHot = true) => + Add(new TValue(Time, Value, IsNew, IsHot)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public virtual void Add(double Value, bool IsNew = true, bool IsHot = true) => + Add(new TValue(DateTime.UtcNow, Value, IsNew, IsHot)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(IEnumerable values) { var valueList = values.ToList(); @@ -82,16 +117,18 @@ public class TSeries : List for (int i = 0; i < count; i++) { - this.Add(startTime, valueList[i]); + Add(startTime, valueList[i]); startTime = startTime.AddHours(1); } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(TSeries series) { if (series == this) { // If adding itself, create a copy to avoid modification during enumeration - var copy = new TSeries { Name = this.Name }; + var copy = new TSeries { Name = Name }; copy.AddRange(this); AddRange(copy); } @@ -100,6 +137,8 @@ public class TSeries : List AddRange(series); } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public new virtual void AddRange(IEnumerable collection) { foreach (var item in collection) @@ -107,5 +146,7 @@ public class TSeries : List Add(item); } } - public void Sub(object source, in ValueEventArgs args) { Add(args.Tick); } -} \ No newline at end of file + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Sub(object source, in ValueEventArgs args) => Add(args.Tick); +} diff --git a/lib/errors/Huber.cs b/lib/errors/Huber.cs index 44d4915f..46bca54d 100644 --- a/lib/errors/Huber.cs +++ b/lib/errors/Huber.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -30,15 +30,18 @@ namespace QuanTAlib; /// https://projecteuclid.org/euclid.aoms/1177703732 /// -public class Huber : AbstractBase +[SkipLocalsInit] +public sealed class Huber : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; private readonly double _delta; + private readonly double _halfDelta; /// The number of points over which to calculate the loss. /// The threshold between squared and linear loss (default 1.0). /// Thrown when period is less than 1 or delta is not positive. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Huber(int period, double delta = 1.0) { if (period < 1) @@ -53,6 +56,7 @@ public class Huber : AbstractBase _actualBuffer = new CircularBuffer(period); _predictedBuffer = new CircularBuffer(period); _delta = delta; + _halfDelta = delta * 0.5; Name = $"Huberloss(period={period}, delta={delta})"; Init(); } @@ -60,12 +64,14 @@ public class Huber : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the loss. /// The threshold between squared and linear loss (default 1.0). + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Huber(object source, int period, double delta = 1.0) : this(period, delta) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -73,6 +79,7 @@ public class Huber : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -82,6 +89,20 @@ public class Huber : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private double CalculateHuberLoss(double error) + { + double absError = Math.Abs(error); + if (absError <= _delta) + { + // Squared error for small deviations + return 0.5 * error * error; + } + // Linear error for large deviations + return _delta * (absError - _halfDelta); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -96,28 +117,17 @@ public class Huber : AbstractBase double huberloss = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumLoss = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { double error = actualValues[i] - predictedValues[i]; - double absError = Math.Abs(error); - - if (absError <= _delta) - { - // Squared error for small deviations - sumLoss += 0.5 * error * error; - } - else - { - // Linear error for large deviations - sumLoss += _delta * (absError - 0.5 * _delta); - } + sumLoss += CalculateHuberLoss(error); } - huberloss = sumLoss / _actualBuffer.Count; + huberloss = sumLoss / actualValues.Length; } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Mae.cs b/lib/errors/Mae.cs index 3fc8efec..61ad8ba1 100644 --- a/lib/errors/Mae.cs +++ b/lib/errors/Mae.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -28,13 +28,15 @@ namespace QuanTAlib; /// https://www.statisticshowto.com/absolute-error/ /// -public class Mae : AbstractBase +[SkipLocalsInit] +public sealed class Mae : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the MAE. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mae(int period) { if (period < 1) @@ -50,12 +52,14 @@ public class Mae : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the MAE. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mae(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -63,6 +67,7 @@ public class Mae : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -72,6 +77,7 @@ public class Mae : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -86,16 +92,16 @@ public class Mae : AbstractBase double mae = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumAbsoluteError = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { sumAbsoluteError += Math.Abs(actualValues[i] - predictedValues[i]); } - mae = sumAbsoluteError / _actualBuffer.Count; + mae = sumAbsoluteError / actualValues.Length; } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Mapd.cs b/lib/errors/Mapd.cs index e36b4889..ad76295d 100644 --- a/lib/errors/Mapd.cs +++ b/lib/errors/Mapd.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -30,13 +30,15 @@ namespace QuanTAlib; /// Note: Also known as MAPE (Mean Absolute Percentage Error) in some contexts /// -public class Mapd : AbstractBase +[SkipLocalsInit] +public sealed class Mapd : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the MAPD. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mapd(int period) { if (period < 1) @@ -52,12 +54,14 @@ public class Mapd : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the MAPD. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mapd(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -65,6 +69,7 @@ public class Mapd : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -74,6 +79,13 @@ public class Mapd : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculatePercentageDeviation(double actual, double predicted) + { + return actual != 0 ? Math.Abs((actual - predicted) / actual) : 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -88,19 +100,16 @@ public class Mapd : AbstractBase double mapd = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumAbsolutePercentageDeviation = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { - if (actualValues[i] != 0) - { - sumAbsolutePercentageDeviation += Math.Abs((actualValues[i] - predictedValues[i]) / actualValues[i]); - } + sumAbsolutePercentageDeviation += CalculatePercentageDeviation(actualValues[i], predictedValues[i]); } - mapd = sumAbsolutePercentageDeviation / _actualBuffer.Count; + mapd = sumAbsolutePercentageDeviation / actualValues.Length; } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Mape.cs b/lib/errors/Mape.cs index 26d2492d..232aeb27 100644 --- a/lib/errors/Mape.cs +++ b/lib/errors/Mape.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -30,13 +30,15 @@ namespace QuanTAlib; /// Note: Also known as MAPD (Mean Absolute Percentage Deviation) in some contexts /// -public class Mape : AbstractBase +[SkipLocalsInit] +public sealed class Mape : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the MAPE. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mape(int period) { if (period < 1) @@ -52,12 +54,14 @@ public class Mape : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the MAPE. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mape(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -65,6 +69,7 @@ public class Mape : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -74,6 +79,13 @@ public class Mape : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculatePercentageError(double actual, double predicted) + { + return actual != 0 ? Math.Abs((actual - predicted) / actual) : 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -88,19 +100,16 @@ public class Mape : AbstractBase double mape = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumAbsolutePercentageError = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { - if (actualValues[i] != 0) - { - sumAbsolutePercentageError += Math.Abs((actualValues[i] - predictedValues[i]) / actualValues[i]); - } + sumAbsolutePercentageError += CalculatePercentageError(actualValues[i], predictedValues[i]); } - mape = sumAbsolutePercentageError / _actualBuffer.Count; + mape = sumAbsolutePercentageError / actualValues.Length; } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Mase.cs b/lib/errors/Mase.cs index 073819ec..be0fe7c1 100644 --- a/lib/errors/Mase.cs +++ b/lib/errors/Mase.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -29,7 +29,8 @@ namespace QuanTAlib; /// https://robjhyndman.com/papers/another-look-at-measures-of-forecast-accuracy/ /// -public class Mase : AbstractBase +[SkipLocalsInit] +public sealed class Mase : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; @@ -37,6 +38,7 @@ public class Mase : AbstractBase /// The number of points over which to calculate the MASE. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mase(int period) { if (period < 1) @@ -53,12 +55,14 @@ public class Mase : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the MASE. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mase(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -67,6 +71,7 @@ public class Mase : AbstractBase _naiveBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -76,6 +81,7 @@ public class Mase : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -103,6 +109,7 @@ public class Mase : AbstractBase /// Calculates the MASE value by comparing forecast error to naive forecast error. /// /// The calculated MASE value, or positive infinity if naive error is zero. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private double CalculateMase() { if (_actualBuffer.Count <= 1) return 0; @@ -112,14 +119,15 @@ public class Mase : AbstractBase ReadOnlySpan naiveValues = _naiveBuffer.GetSpan(); double sumAbsoluteError = CalculateSumAbsoluteError(actualValues, predictedValues); - double _naiveForecastError = CalculateNaiveForecastError(actualValues, naiveValues); + double naiveForecastError = CalculateNaiveForecastError(actualValues, naiveValues); - return _naiveForecastError != 0 ? (sumAbsoluteError / _actualBuffer.Count) / _naiveForecastError : double.PositiveInfinity; + return naiveForecastError != 0 ? (sumAbsoluteError / _actualBuffer.Count) / naiveForecastError : double.PositiveInfinity; } /// /// Calculates the sum of absolute errors between actual and predicted values. /// + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static double CalculateSumAbsoluteError(ReadOnlySpan actualValues, ReadOnlySpan predictedValues) { double sum = 0; @@ -133,6 +141,7 @@ public class Mase : AbstractBase /// /// Calculates the naive forecast error using the previous value as prediction. /// + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static double CalculateNaiveForecastError(ReadOnlySpan actualValues, ReadOnlySpan naiveValues) { double sum = 0; diff --git a/lib/errors/Mda.cs b/lib/errors/Mda.cs index 332337b8..cbff518a 100644 --- a/lib/errors/Mda.cs +++ b/lib/errors/Mda.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -30,13 +30,15 @@ namespace QuanTAlib; /// "Evaluating Forecasting Performance" - International Journal of Forecasting /// -public class Mda : AbstractBase +[SkipLocalsInit] +public sealed class Mda : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the MDA. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mda(int period) { if (period < 1) @@ -52,12 +54,14 @@ public class Mda : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the MDA. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mda(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -65,6 +69,7 @@ public class Mda : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -74,6 +79,13 @@ public class Mda : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static int CompareDirections(double current, double previous) + { + return Math.Sign(current - previous); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -88,18 +100,18 @@ public class Mda : AbstractBase double mda = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumDirectionalAccuracy = 0; - for (int i = 1; i < _actualBuffer.Count; i++) + for (int i = 1; i < actualValues.Length; i++) { - double actualDirection = Math.Sign(actualValues[i] - actualValues[i - 1]); - double predictedDirection = Math.Sign(predictedValues[i] - predictedValues[i - 1]); + int actualDirection = CompareDirections(actualValues[i], actualValues[i - 1]); + int predictedDirection = CompareDirections(predictedValues[i], predictedValues[i - 1]); sumDirectionalAccuracy += (actualDirection == predictedDirection) ? 1 : 0; } - mda = sumDirectionalAccuracy / (_actualBuffer.Count - 1); + mda = sumDirectionalAccuracy / (actualValues.Length - 1); } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Me.cs b/lib/errors/Me.cs index ab75705a..c4422a29 100644 --- a/lib/errors/Me.cs +++ b/lib/errors/Me.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -30,13 +30,15 @@ namespace QuanTAlib; /// Note: Also known as Mean Bias Error (MBE) or Mean Signed Difference (MSD) /// -public class Me : AbstractBase +[SkipLocalsInit] +public sealed class Me : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the ME. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Me(int period) { if (period < 1) @@ -52,12 +54,14 @@ public class Me : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the ME. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Me(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -65,6 +69,7 @@ public class Me : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -74,6 +79,13 @@ public class Me : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateError(double actual, double predicted) + { + return actual - predicted; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -88,16 +100,16 @@ public class Me : AbstractBase double me = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumError = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { - sumError += actualValues[i] - predictedValues[i]; + sumError += CalculateError(actualValues[i], predictedValues[i]); } - me = sumError / _actualBuffer.Count; + me = sumError / actualValues.Length; } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Mpe.cs b/lib/errors/Mpe.cs index cf18b88d..8ff5f825 100644 --- a/lib/errors/Mpe.cs +++ b/lib/errors/Mpe.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -31,13 +31,15 @@ namespace QuanTAlib; /// Note: Similar to MAPE but allows error cancellation /// -public class Mpe : AbstractBase +[SkipLocalsInit] +public sealed class Mpe : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the MPE. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mpe(int period) { if (period < 1) @@ -53,12 +55,14 @@ public class Mpe : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the MPE. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mpe(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -66,6 +70,7 @@ public class Mpe : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -75,6 +80,13 @@ public class Mpe : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculatePercentageError(double actual, double predicted) + { + return actual != 0 ? (actual - predicted) / actual : 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -89,19 +101,16 @@ public class Mpe : AbstractBase double mpe = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumPercentageError = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { - if (actualValues[i] != 0) - { - sumPercentageError += (actualValues[i] - predictedValues[i]) / actualValues[i]; - } + sumPercentageError += CalculatePercentageError(actualValues[i], predictedValues[i]); } - mpe = sumPercentageError / _actualBuffer.Count; + mpe = sumPercentageError / actualValues.Length; } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Mse.cs b/lib/errors/Mse.cs index 2c8fa2f6..2ca3949f 100644 --- a/lib/errors/Mse.cs +++ b/lib/errors/Mse.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -30,13 +30,15 @@ namespace QuanTAlib; /// Note: Often used in optimization due to its mathematical properties /// -public class Mse : AbstractBase +[SkipLocalsInit] +public sealed class Mse : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the MSE. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mse(int period) { if (period < 1) @@ -52,12 +54,14 @@ public class Mse : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the MSE. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mse(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -65,6 +69,7 @@ public class Mse : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -74,6 +79,14 @@ public class Mse : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateSquaredError(double actual, double predicted) + { + double error = actual - predicted; + return error * error; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -88,17 +101,16 @@ public class Mse : AbstractBase double mse = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumSquaredError = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { - double error = actualValues[i] - predictedValues[i]; - sumSquaredError += error * error; + sumSquaredError += CalculateSquaredError(actualValues[i], predictedValues[i]); } - mse = sumSquaredError / _actualBuffer.Count; + mse = sumSquaredError / actualValues.Length; } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Msle.cs b/lib/errors/Msle.cs index 27da073b..cc8bea85 100644 --- a/lib/errors/Msle.cs +++ b/lib/errors/Msle.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -31,13 +31,15 @@ namespace QuanTAlib; /// Note: Often used in cases where target values follow exponential growth /// -public class Msle : AbstractBase +[SkipLocalsInit] +public sealed class Msle : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the MSLE. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Msle(int period) { if (period < 1) @@ -53,12 +55,14 @@ public class Msle : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the MSLE. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Msle(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -66,6 +70,7 @@ public class Msle : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -75,6 +80,16 @@ public class Msle : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateSquaredLogError(double actual, double predicted) + { + double logActual = Math.Log(actual + 1); + double logPredicted = Math.Log(predicted + 1); + double error = logActual - logPredicted; + return error * error; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -89,19 +104,16 @@ public class Msle : AbstractBase double msle = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumSquaredLogError = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { - double logActual = Math.Log(actualValues[i] + 1); - double logPredicted = Math.Log(predictedValues[i] + 1); - double error = logActual - logPredicted; - sumSquaredLogError += error * error; + sumSquaredLogError += CalculateSquaredLogError(actualValues[i], predictedValues[i]); } - msle = sumSquaredLogError / _actualBuffer.Count; + msle = sumSquaredLogError / actualValues.Length; } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Rae.cs b/lib/errors/Rae.cs index a9567a02..5f5b69d1 100644 --- a/lib/errors/Rae.cs +++ b/lib/errors/Rae.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -30,13 +30,15 @@ namespace QuanTAlib; /// Note: Values greater than 1 indicate predictions worse than using zero /// -public class Rae : AbstractBase +[SkipLocalsInit] +public sealed class Rae : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the RAE. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rae(int period) { if (period < 1) @@ -52,12 +54,14 @@ public class Rae : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the RAE. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rae(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -65,6 +69,7 @@ public class Rae : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -74,6 +79,13 @@ public class Rae : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double error, double magnitude) CalculateErrorAndMagnitude(double actual, double predicted) + { + return (Math.Abs(actual - predicted), Math.Abs(actual)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -88,18 +100,19 @@ public class Rae : AbstractBase double rae = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumAbsoluteError = 0; double sumAbsoluteActual = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { - sumAbsoluteError += Math.Abs(actualValues[i] - predictedValues[i]); - sumAbsoluteActual += Math.Abs(actualValues[i]); + var (error, magnitude) = CalculateErrorAndMagnitude(actualValues[i], predictedValues[i]); + sumAbsoluteError += error; + sumAbsoluteActual += magnitude; } - rae = sumAbsoluteError / sumAbsoluteActual; + rae = sumAbsoluteActual > 0 ? sumAbsoluteError / sumAbsoluteActual : 0; } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Rmse.cs b/lib/errors/Rmse.cs index 1474fd12..04c873c0 100644 --- a/lib/errors/Rmse.cs +++ b/lib/errors/Rmse.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -31,13 +31,15 @@ namespace QuanTAlib; /// Note: Square root of MSE, making it more interpretable in original units /// -public class Rmse : AbstractBase +[SkipLocalsInit] +public sealed class Rmse : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the RMSE. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rmse(int period) { if (period < 1) @@ -53,12 +55,14 @@ public class Rmse : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the RMSE. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rmse(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -66,6 +70,7 @@ public class Rmse : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -75,6 +80,14 @@ public class Rmse : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateSquaredError(double actual, double predicted) + { + double error = actual - predicted; + return error * error; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -89,17 +102,16 @@ public class Rmse : AbstractBase double rmse = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumSquaredError = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { - double error = actualValues[i] - predictedValues[i]; - sumSquaredError += error * error; + sumSquaredError += CalculateSquaredError(actualValues[i], predictedValues[i]); } - rmse = Math.Sqrt(sumSquaredError / _actualBuffer.Count); + rmse = Math.Sqrt(sumSquaredError / actualValues.Length); } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Rmsle.cs b/lib/errors/Rmsle.cs index 846e8a65..79c66256 100644 --- a/lib/errors/Rmsle.cs +++ b/lib/errors/Rmsle.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -32,13 +32,15 @@ namespace QuanTAlib; /// Note: Square root of MSLE, useful for data with exponential growth /// -public class Rmsle : AbstractBase +[SkipLocalsInit] +public sealed class Rmsle : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the RMSLE. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rmsle(int period) { if (period < 1) @@ -54,12 +56,14 @@ public class Rmsle : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the RMSLE. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rmsle(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -67,6 +71,7 @@ public class Rmsle : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -76,6 +81,16 @@ public class Rmsle : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateSquaredLogError(double actual, double predicted) + { + double logActual = Math.Log(actual + 1); + double logPredicted = Math.Log(predicted + 1); + double error = logActual - logPredicted; + return error * error; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -90,19 +105,16 @@ public class Rmsle : AbstractBase double rmsle = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumSquaredLogError = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { - double logActual = Math.Log(actualValues[i] + 1); - double logPredicted = Math.Log(predictedValues[i] + 1); - double error = logActual - logPredicted; - sumSquaredLogError += error * error; + sumSquaredLogError += CalculateSquaredLogError(actualValues[i], predictedValues[i]); } - rmsle = Math.Sqrt(sumSquaredLogError / _actualBuffer.Count); + rmsle = Math.Sqrt(sumSquaredLogError / actualValues.Length); } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Rse.cs b/lib/errors/Rse.cs index 0074cd1b..61e87f26 100644 --- a/lib/errors/Rse.cs +++ b/lib/errors/Rse.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -31,13 +30,15 @@ namespace QuanTAlib; /// Note: Values less than 1 indicate predictions better than using mean /// -public class Rse : AbstractBase +[SkipLocalsInit] +public sealed class Rse : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the RSE. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rse(int period) { if (period < 1) @@ -53,12 +54,14 @@ public class Rse : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the RSE. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rse(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -66,6 +69,7 @@ public class Rse : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -75,6 +79,15 @@ public class Rse : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double squaredError, double squaredDeviation) CalculateErrors(double actual, double predicted, double meanActual) + { + double error = actual - predicted; + double deviation = actual - meanActual; + return (error * error, deviation * deviation); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -89,22 +102,21 @@ public class Rse : AbstractBase double rse = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumSquaredError = 0; double sumSquaredActual = 0; - double meanActual = actualValues.Average(); + double meanActual = _actualBuffer.Average(); - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { - double error = actualValues[i] - predictedValues[i]; - sumSquaredError += error * error; - double deviation = actualValues[i] - meanActual; - sumSquaredActual += deviation * deviation; + var (squaredError, squaredDeviation) = CalculateErrors(actualValues[i], predictedValues[i], meanActual); + sumSquaredError += squaredError; + sumSquaredActual += squaredDeviation; } - rse = Math.Sqrt(sumSquaredError / sumSquaredActual); + rse = sumSquaredActual > 0 ? Math.Sqrt(sumSquaredError / sumSquaredActual) : 0; } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Rsquared.cs b/lib/errors/Rsquared.cs index c7994def..f7fa5e80 100644 --- a/lib/errors/Rsquared.cs +++ b/lib/errors/Rsquared.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -31,13 +30,15 @@ namespace QuanTAlib; /// Note: Can be negative if predictions are worse than using the mean /// -public class Rsquared : AbstractBase +[SkipLocalsInit] +public sealed class Rsquared : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; /// The number of points over which to calculate the R-squared value. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rsquared(int period) { if (period < 1) @@ -53,12 +54,14 @@ public class Rsquared : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the R-squared value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rsquared(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -66,6 +69,7 @@ public class Rsquared : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -75,6 +79,15 @@ public class Rsquared : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double squaredResidual, double squaredTotal) CalculateSquaredErrors(double actual, double predicted, double meanActual) + { + double deviation = actual - meanActual; + double error = actual - predicted; + return (error * error, deviation * deviation); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -89,25 +102,21 @@ public class Rsquared : AbstractBase double rsquared = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); - double meanActual = actualValues.Average(); + double meanActual = _actualBuffer.Average(); double sumSquaredTotal = 0; double sumSquaredResidual = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { - double deviation = actualValues[i] - meanActual; - sumSquaredTotal += deviation * deviation; - double error = actualValues[i] - predictedValues[i]; - sumSquaredResidual += error * error; + var (squaredResidual, squaredTotal) = CalculateSquaredErrors(actualValues[i], predictedValues[i], meanActual); + sumSquaredResidual += squaredResidual; + sumSquaredTotal += squaredTotal; } - if (sumSquaredTotal != 0) - { - rsquared = 1 - (sumSquaredResidual / sumSquaredTotal); - } + rsquared = sumSquaredTotal != 0 ? 1 - (sumSquaredResidual / sumSquaredTotal) : 0; } IsHot = _index >= WarmupPeriod; diff --git a/lib/errors/Smape.cs b/lib/errors/Smape.cs index 73b47bb5..0aecc8dd 100644 --- a/lib/errors/Smape.cs +++ b/lib/errors/Smape.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -30,13 +30,16 @@ namespace QuanTAlib; /// Note: More stable than MAPE when actual values are close to zero /// -public class Smape : AbstractBase +[SkipLocalsInit] +public sealed class Smape : AbstractBase { private readonly CircularBuffer _actualBuffer; private readonly CircularBuffer _predictedBuffer; + private const double Epsilon = 1e-10; /// The number of points over which to calculate the SMAPE. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Smape(int period) { if (period < 1) @@ -52,12 +55,14 @@ public class Smape : AbstractBase /// The data source object that publishes updates. /// The number of points over which to calculate the SMAPE. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Smape(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -65,6 +70,7 @@ public class Smape : AbstractBase _predictedBuffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -74,6 +80,14 @@ public class Smape : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateSymmetricError(double actual, double predicted) + { + double denominator = Math.Abs(actual) + Math.Abs(predicted); + return denominator > Epsilon ? Math.Abs(actual - predicted) / denominator : 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -88,18 +102,18 @@ public class Smape : AbstractBase double smape = 0; if (_actualBuffer.Count > 0) { - var actualValues = _actualBuffer.GetSpan().ToArray(); - var predictedValues = _predictedBuffer.GetSpan().ToArray(); + ReadOnlySpan actualValues = _actualBuffer.GetSpan(); + ReadOnlySpan predictedValues = _predictedBuffer.GetSpan(); double sumSymmetricAbsolutePercentageError = 0; int validCount = 0; - for (int i = 0; i < _actualBuffer.Count; i++) + for (int i = 0; i < actualValues.Length; i++) { - double denominator = Math.Abs(actualValues[i]) + Math.Abs(predictedValues[i]); - if (denominator != 0) + double error = CalculateSymmetricError(actualValues[i], predictedValues[i]); + if (error > 0) { - sumSymmetricAbsolutePercentageError += Math.Abs(actualValues[i] - predictedValues[i]) / denominator; + sumSymmetricAbsolutePercentageError += error; validCount++; } } diff --git a/lib/oscillators/Cmo.cs b/lib/oscillators/Cmo.cs index a303f67e..cd10da8c 100644 --- a/lib/oscillators/Cmo.cs +++ b/lib/oscillators/Cmo.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -34,14 +34,18 @@ namespace QuanTAlib; /// Note: Similar to RSI but with different scaling and calculation method /// -public class Cmo : AbstractBase +[SkipLocalsInit] +public sealed class Cmo : AbstractBase { private readonly CircularBuffer _sumH; private readonly CircularBuffer _sumL; private double _prevValue, _p_prevValue; + private const double Epsilon = 1e-10; + private const double ScalingFactor = 100.0; /// The number of periods used in the CMO calculation. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Cmo(int period) { if (period < 1) @@ -55,12 +59,14 @@ public class Cmo : AbstractBase /// The data source object that publishes updates. /// The number of periods used in the CMO calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Cmo(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -74,6 +80,20 @@ public class Cmo : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double up, double down) CalculateMovements(double diff) + { + return diff > 0 ? (diff, 0) : (0, -diff); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateCmo(double sumH, double sumL) + { + double divisor = sumH + sumL; + return (Math.Abs(divisor) > Epsilon) ? ScalingFactor * ((sumH - sumL) / divisor) : 0.0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -88,25 +108,11 @@ public class Cmo : AbstractBase _prevValue = Input.Value; // Separate upward and downward movements - if (diff > 0) - { - _sumH.Add(diff, Input.IsNew); - _sumL.Add(0, Input.IsNew); - } - else - { - _sumH.Add(0, Input.IsNew); - _sumL.Add(-diff, Input.IsNew); - } + var (up, down) = CalculateMovements(diff); + _sumH.Add(up, Input.IsNew); + _sumL.Add(down, Input.IsNew); - // Calculate sums for the specified period - double sumH = _sumH.Sum(); - double sumL = _sumL.Sum(); - double divisor = sumH + sumL; - - // Calculate CMO value - return (Math.Abs(divisor) > double.Epsilon) ? - 100.0 * ((sumH - sumL) / divisor) : - 0.0; + // Calculate sums and CMO value + return CalculateCmo(_sumH.Sum(), _sumL.Sum()); } } diff --git a/lib/oscillators/Rsi.cs b/lib/oscillators/Rsi.cs index 5f61fe27..324f631f 100644 --- a/lib/oscillators/Rsi.cs +++ b/lib/oscillators/Rsi.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -35,15 +35,19 @@ namespace QuanTAlib; /// Note: Default period of 14 was recommended by Wilder /// -public class Rsi : AbstractBase +[SkipLocalsInit] +public sealed class Rsi : AbstractBase { private readonly Rma _avgGain; private readonly Rma _avgLoss; private double _prevValue, _p_prevValue; + private const double ScalingFactor = 100.0; + private const int DefaultPeriod = 14; /// The number of periods used in the RSI calculation (default 14). /// Thrown when period is less than 1. - public Rsi(int period = 14) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rsi(int period = DefaultPeriod) { if (period < 1) throw new ArgumentOutOfRangeException(nameof(period)); @@ -56,12 +60,14 @@ public class Rsi : AbstractBase /// The data source object that publishes updates. /// The number of periods used in the RSI calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rsi(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -75,6 +81,19 @@ public class Rsi : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double gain, double loss) CalculateGainLoss(double change) + { + return (Math.Max(change, 0), Math.Max(-change, 0)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateRsi(double avgGain, double avgLoss) + { + return avgLoss > 0 ? ScalingFactor - (ScalingFactor / (1 + (avgGain / avgLoss))) : ScalingFactor; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -86,17 +105,14 @@ public class Rsi : AbstractBase // Calculate price change and separate gains/losses double change = Input.Value - _prevValue; - double gain = Math.Max(change, 0); - double loss = Math.Max(-change, 0); + var (gain, loss) = CalculateGainLoss(change); _prevValue = Input.Value; // Calculate smoothed averages using Wilder's method - _avgGain.Calc(gain, IsNew: Input.IsNew); - _avgLoss.Calc(loss, IsNew: Input.IsNew); + _avgGain.Calc(gain, Input.IsNew); + _avgLoss.Calc(loss, Input.IsNew); // Calculate RSI - double rsi = (_avgLoss.Value > 0) ? 100 - (100 / (1 + (_avgGain.Value / _avgLoss.Value))) : 100; - - return rsi; + return CalculateRsi(_avgGain.Value, _avgLoss.Value); } } diff --git a/lib/oscillators/Rsx.cs b/lib/oscillators/Rsx.cs index 6401c244..55d6cfd9 100644 --- a/lib/oscillators/Rsx.cs +++ b/lib/oscillators/Rsx.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -34,24 +34,34 @@ namespace QuanTAlib; /// Note: Proprietary enhancement of RSI using JMA technology /// -public class Rsx : AbstractBase +[SkipLocalsInit] +public sealed class Rsx : AbstractBase { private readonly Rma _avgGain; private readonly Rma _avgLoss; private readonly Jma _rsx; private double _prevValue, _p_prevValue; + private const double ScalingFactor = 100.0; + private const int DefaultPeriod = 14; + private const int DefaultPhase = 0; + private const double DefaultFactor = 0.55; + private const int JmaPeriod = 8; + private const int JmaPower = 100; + private const double JmaPhase = 0.25; + private const int JmaExtra = 3; /// The number of periods for RSI calculation (default 14). /// The phase parameter for JMA smoothing (default 0). /// The factor parameter for smoothing control (default 0.55). /// Thrown when period is less than 1. - public Rsx(int period = 14, int phase = 0, double factor = 0.55) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rsx(int period = DefaultPeriod, int phase = DefaultPhase, double factor = DefaultFactor) { if (period < 1) throw new ArgumentOutOfRangeException(nameof(period)); _avgGain = new(period); _avgLoss = new(period); - _rsx = new(8, 100, 0.25, 3); + _rsx = new(JmaPeriod, JmaPower, JmaPhase, JmaExtra); _index = 0; WarmupPeriod = period + 1; Name = $"RSX({period})"; @@ -61,12 +71,14 @@ public class Rsx : AbstractBase /// The number of periods for RSI calculation. /// The phase parameter for JMA smoothing. /// The factor parameter for smoothing control. - public Rsx(object source, int period, int phase = 0, double factor = 0.55) : this(period, phase, factor) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rsx(object source, int period, int phase = DefaultPhase, double factor = DefaultFactor) : this(period, phase, factor) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -80,6 +92,19 @@ public class Rsx : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double gain, double loss) CalculateGainLoss(double change) + { + return (Math.Max(change, 0), Math.Max(-change, 0)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateRsi(double avgGain, double avgLoss) + { + return avgLoss > 0 ? ScalingFactor - (ScalingFactor / (1 + (avgGain / avgLoss))) : ScalingFactor; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -91,18 +116,17 @@ public class Rsx : AbstractBase // Calculate RSI components double change = Input.Value - _prevValue; - double gain = Math.Max(change, 0); - double loss = Math.Max(-change, 0); + var (gain, loss) = CalculateGainLoss(change); _prevValue = Input.Value; // Calculate RSI - _avgGain.Calc(gain, IsNew: Input.IsNew); - _avgLoss.Calc(loss, IsNew: Input.IsNew); - double rsi = (_avgLoss.Value > 0) ? 100 - (100 / (1 + (_avgGain.Value / _avgLoss.Value))) : 100; + _avgGain.Calc(gain, Input.IsNew); + _avgLoss.Calc(loss, Input.IsNew); + double rsi = CalculateRsi(_avgGain.Value, _avgLoss.Value); // Apply JMA smoothing - double rsx = _rsx.Calc(rsi, Input.IsNew); + _rsx.Calc(rsi, Input.IsNew); - return rsx; + return _rsx.Value; } } diff --git a/lib/quantalib.csproj b/lib/quantalib.csproj index 3f706bc7..5b87dfa0 100644 --- a/lib/quantalib.csproj +++ b/lib/quantalib.csproj @@ -15,7 +15,6 @@ 0.0.0.0 True AnyCPU - False full True True diff --git a/lib/statistics/Curvature.cs b/lib/statistics/Curvature.cs index 2d51c8d3..157b5fd6 100644 --- a/lib/statistics/Curvature.cs +++ b/lib/statistics/Curvature.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -36,11 +35,13 @@ namespace QuanTAlib; /// Note: Second-order derivative providing acceleration insights /// -public class Curvature : AbstractBase +[SkipLocalsInit] +public sealed class Curvature : AbstractBase { private readonly int _period; private readonly Slope _slopeCalculator; private readonly CircularBuffer _slopeBuffer; + private const double Epsilon = 1e-10; /// /// Gets the y-intercept of the curvature line. @@ -64,6 +65,7 @@ public class Curvature : AbstractBase /// The number of points to consider for calculation. /// Thrown when period is 2 or less. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Curvature(int period) { if (period <= 2) @@ -82,12 +84,14 @@ public class Curvature : AbstractBase /// The data source object that publishes updates. /// The number of points to consider for calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Curvature(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -98,6 +102,7 @@ public class Curvature : AbstractBase Line = null; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -107,6 +112,35 @@ public class Curvature : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double sumX, double sumY) CalculateSums(ReadOnlySpan slopes, int count) + { + double sumX = 0, sumY = 0; + for (int i = 0; i < count; i++) + { + sumX += i + 1; + sumY += slopes[i]; + } + return (sumX, sumY); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double sumSqX, double sumSqY, double sumSqXY) CalculateSquaredSums( + ReadOnlySpan slopes, int count, double avgX, double avgY) + { + double sumSqX = 0, sumSqY = 0, sumSqXY = 0; + for (int i = 0; i < count; i++) + { + double devX = (i + 1) - avgX; + double devY = slopes[i] - avgY; + sumSqX += devX * devX; + sumSqY += devY * devY; + sumSqXY += devX * devY; + } + return (sumSqX, sumSqY, sumSqXY); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -122,30 +156,17 @@ public class Curvature : AbstractBase } int count = Math.Min(_slopeBuffer.Count, _period); - var slopes = _slopeBuffer.GetSpan().ToArray(); + ReadOnlySpan slopes = _slopeBuffer.GetSpan(); // Calculate averages - double sumX = 0, sumY = 0; - for (int i = 0; i < count; i++) - { - sumX += i + 1; - sumY += slopes[i]; - } + var (sumX, sumY) = CalculateSums(slopes, count); double avgX = sumX / count; double avgY = sumY / count; // Least squares method - double sumSqX = 0, sumSqY = 0, sumSqXY = 0; - for (int i = 0; i < count; i++) - { - double devX = (i + 1) - avgX; - double devY = slopes[i] - avgY; - sumSqX += devX * devX; - sumSqY += devY * devY; - sumSqXY += devX * devY; - } + var (sumSqX, sumSqY, sumSqXY) = CalculateSquaredSums(slopes, count, avgX, avgY); - if (sumSqX > 0) + if (sumSqX > Epsilon) { curvature = sumSqXY / sumSqX; Intercept = avgY - (curvature * avgX); @@ -155,9 +176,10 @@ public class Curvature : AbstractBase double stdDevY = Math.Sqrt(sumSqY / count); StdDev = stdDevY; - if (stdDevX * stdDevY != 0) + double stdDevProduct = stdDevX * stdDevY; + if (stdDevProduct > Epsilon) { - double r = sumSqXY / (stdDevX * stdDevY) / count; + double r = sumSqXY / (stdDevProduct) / count; RSquared = r * r; } diff --git a/lib/statistics/Entropy.cs b/lib/statistics/Entropy.cs index f388b183..82f15cb4 100644 --- a/lib/statistics/Entropy.cs +++ b/lib/statistics/Entropy.cs @@ -1,5 +1,5 @@ -using System; -using System.Linq; +using System.Collections.Generic; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -41,41 +41,52 @@ namespace QuanTAlib; /// Note: Normalized to [0,1] for easier interpretation /// -public class Entropy : AbstractBase +[SkipLocalsInit] +public sealed class Entropy : AbstractBase { private readonly int Period; private readonly CircularBuffer _buffer; + private readonly Dictionary _valueCounts; + private const double Epsilon = 1e-10; + private const double DefaultEntropy = 1.0; + private const int MinimumPoints = 2; /// The number of points to consider for entropy calculation. /// Thrown when period is less than 2. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Entropy(int period) { - if (period < 2) + if (period < MinimumPoints) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2 for entropy calculation."); } Period = period; - WarmupPeriod = 2; // Minimum number of points needed for entropy calculation + WarmupPeriod = MinimumPoints; // Minimum number of points needed for entropy calculation _buffer = new CircularBuffer(period); + _valueCounts = new Dictionary(); Name = $"Entropy(period={period})"; Init(); } /// The data source object that publishes updates. /// The number of points to consider for entropy calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Entropy(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _buffer.Clear(); + _valueCounts.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -85,39 +96,50 @@ public class Entropy : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static void CountValues(ReadOnlySpan values, Dictionary counts) + { + counts.Clear(); + for (int i = 0; i < values.Length; i++) + { + counts[values[i]] = counts.TryGetValue(values[i], out int count) ? count + 1 : 1; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateShannonsEntropy(Dictionary counts, int totalCount) + { + double entropy = 0; + foreach (var count in counts.Values) + { + double probability = (double)count / totalCount; + entropy -= probability * Math.Log2(probability); + } + return entropy; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); _buffer.Add(Input.Value, Input.IsNew); - double entropy = 0; - if (_index > 1) // Need at least two data points for entropy calculation + if (_index <= 1) // Need at least two data points for entropy calculation { - var values = _buffer.GetSpan().ToArray(); - int n = values.Length; - - // Calculate probabilities for each unique value - var groupedValues = values.GroupBy(x => x).Select(g => new { Value = g.Key, Count = g.Count() }); - - // Calculate Shannon's entropy - foreach (var group in groupedValues) - { - double probability = (double)group.Count / n; - entropy -= probability * Math.Log2(probability); - } - - // Normalize by maximum possible entropy for current unique values - int uniqueValueCount = groupedValues.Count(); - double maxEntropy = Math.Log2(uniqueValueCount); - - entropy = entropy == 0 ? 1 : entropy / maxEntropy; - } - else - { - entropy = 1; // Maximum entropy when insufficient data + return DefaultEntropy; } + ReadOnlySpan values = _buffer.GetSpan(); + CountValues(values, _valueCounts); + + // Calculate Shannon's entropy + double entropy = CalculateShannonsEntropy(_valueCounts, values.Length); + + // Normalize by maximum possible entropy for current unique values + double maxEntropy = Math.Log2(_valueCounts.Count); + entropy = maxEntropy < Epsilon ? DefaultEntropy : entropy / maxEntropy; + IsHot = _buffer.Count >= Period; return entropy; } diff --git a/lib/statistics/Kurtosis.cs b/lib/statistics/Kurtosis.cs index 0166b65b..023cae36 100644 --- a/lib/statistics/Kurtosis.cs +++ b/lib/statistics/Kurtosis.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -42,16 +41,20 @@ namespace QuanTAlib; /// Note: Returns excess kurtosis (normal distribution = 0) /// -public class Kurtosis : AbstractBase +[SkipLocalsInit] +public sealed class Kurtosis : AbstractBase { private readonly int Period; private readonly CircularBuffer _buffer; + private const double Epsilon = 1e-10; + private const int MinimumPoints = 4; /// The number of points to consider for kurtosis calculation. /// Thrown when period is less than 4. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Kurtosis(int period) { - if (period < 4) + if (period < MinimumPoints) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 4 for kurtosis calculation."); @@ -65,18 +68,21 @@ public class Kurtosis : AbstractBase /// The data source object that publishes updates. /// The number of points to consider for kurtosis calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Kurtosis(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _buffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -86,6 +92,48 @@ public class Kurtosis : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateMean(ReadOnlySpan values) + { + double sum = 0; + for (int i = 0; i < values.Length; i++) + { + sum += values[i]; + } + return sum / values.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double s2, double s4) CalculateDeviations(ReadOnlySpan values, double mean) + { + double s2 = 0; // Sum of squared deviations + double s4 = 0; // Sum of fourth power deviations + + for (int i = 0; i < values.Length; i++) + { + double diff = values[i] - mean; + double diff2 = diff * diff; + s2 += diff2; + s4 += diff2 * diff2; + } + + return (s2, s4); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateSheskinKurtosis(double s2, double s4, int n) + { + double variance = s2 / (n - 1); + double variance2 = variance * variance; + + if (variance2 < Epsilon) + return 0; + + return (n * (n + 1) * s4) / (variance2 * (n - 3) * (n - 1) * (n - 2)) + - (3 * (n - 1) * (n - 1) / ((n - 2) * (n - 3))); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -93,28 +141,12 @@ public class Kurtosis : AbstractBase _buffer.Add(Input.Value, Input.IsNew); double kurtosis = 0; - if (_buffer.Count > 3) // Need at least 4 points for valid calculation + if (_buffer.Count > MinimumPoints - 1) // Need at least 4 points for valid calculation { - var values = _buffer.GetSpan().ToArray(); - double mean = values.Average(); - double n = values.Length; - - // Calculate squared and fourth power deviations - double s2 = 0; // Sum of squared deviations - double s4 = 0; // Sum of fourth power deviations - - for (int i = 0; i < values.Length; i++) - { - double diff = values[i] - mean; - s2 += diff * diff; - s4 += diff * diff * diff * diff; - } - - double variance = s2 / (n - 1); - - // Sheskin Algorithm for excess kurtosis - kurtosis = (n * (n + 1) * s4) / (variance * variance * (n - 3) * (n - 1) * (n - 2)) - - (3 * (n - 1) * (n - 1) / ((n - 2) * (n - 3))); + ReadOnlySpan values = _buffer.GetSpan(); + double mean = CalculateMean(values); + var (s2, s4) = CalculateDeviations(values, mean); + kurtosis = CalculateSheskinKurtosis(s2, s4, values.Length); } IsHot = _buffer.Count >= Period; diff --git a/lib/statistics/Max.cs b/lib/statistics/Max.cs index 13ae8ad1..acbfc161 100644 --- a/lib/statistics/Max.cs +++ b/lib/statistics/Max.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -40,7 +40,8 @@ namespace QuanTAlib; /// Note: Decay factor allows for adaptive peak tracking /// -public class Max : AbstractBase +[SkipLocalsInit] +public sealed class Max : AbstractBase { private readonly int Period; private readonly CircularBuffer _buffer; @@ -49,11 +50,15 @@ public class Max : AbstractBase private double _p_currentMax; private int _timeSinceNewMax; private int _p_timeSinceNewMax; + private const double DefaultDecay = 0.0; + private const double DecayScaleFactor = 0.1; + private const double Epsilon = 1e-10; /// The number of points to consider for maximum calculation. /// Half-life decay factor (0 for no decay, higher for faster forgetting). /// Thrown when period is less than 1 or decay is negative. - public Max(int period, double decay = 0) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Max(int period, double decay = DefaultDecay) { if (period < 1) { @@ -68,7 +73,7 @@ public class Max : AbstractBase Period = period; WarmupPeriod = 0; _buffer = new CircularBuffer(period); - _halfLife = decay * 0.1; + _halfLife = decay * DecayScaleFactor; Name = $"Max(period={period}, halfLife={decay:F2})"; Init(); } @@ -76,12 +81,14 @@ public class Max : AbstractBase /// The data source object that publishes updates. /// The number of points to consider for maximum calculation. /// Half-life decay factor (default 0). - public Max(object source, int period, double decay = 0) : this(period, decay) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Max(object source, int period, double decay = DefaultDecay) : this(period, decay) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -89,6 +96,7 @@ public class Max : AbstractBase _timeSinceNewMax = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -106,6 +114,27 @@ public class Max : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private double CalculateDecayRate() + { + return 1 - Math.Exp(-_halfLife * _timeSinceNewMax / Period); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double FindMaxValue(ReadOnlySpan values) + { + double max = double.MinValue; + for (int i = 0; i < values.Length; i++) + { + if (values[i] > max) + { + max = values[i]; + } + } + return max; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -119,11 +148,12 @@ public class Max : AbstractBase } // Apply decay based on time since last maximum - double decayRate = 1 - Math.Exp(-_halfLife * _timeSinceNewMax / Period); + double decayRate = CalculateDecayRate(); _currentMax -= decayRate * (_currentMax - _buffer.Average()); // Ensure maximum doesn't exceed current period's highest value - _currentMax = Math.Min(_currentMax, _buffer.Max()); + ReadOnlySpan values = _buffer.GetSpan(); + _currentMax = Math.Min(_currentMax, FindMaxValue(values)); IsHot = true; return _currentMax; diff --git a/lib/statistics/Median.cs b/lib/statistics/Median.cs index fc203704..c8593405 100644 --- a/lib/statistics/Median.cs +++ b/lib/statistics/Median.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -40,13 +39,15 @@ namespace QuanTAlib; /// Note: More robust than mean for non-normal distributions /// -public class Median : AbstractBase +[SkipLocalsInit] +public sealed class Median : AbstractBase { private readonly int Period; private readonly CircularBuffer _buffer; /// The number of points to consider for median calculation. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Median(int period) { if (period < 1) @@ -63,18 +64,21 @@ public class Median : AbstractBase /// The data source object that publishes updates. /// The number of points to consider for median calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Median(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _buffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -84,6 +88,46 @@ public class Median : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static void QuickSort(Span arr, int left, int right) + { + if (left < right) + { + int pivotIndex = Partition(arr, left, right); + QuickSort(arr, left, pivotIndex - 1); + QuickSort(arr, pivotIndex + 1, right); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static int Partition(Span arr, int left, int right) + { + double pivot = arr[right]; + int i = left - 1; + + for (int j = left; j < right; j++) + { + if (arr[j] <= pivot) + { + i++; + (arr[i], arr[j]) = (arr[j], arr[i]); + } + } + + (arr[i + 1], arr[right]) = (arr[right], arr[i + 1]); + return i + 1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateMedian(Span sortedValues) + { + int middleIndex = sortedValues.Length / 2; + return (sortedValues.Length % 2 == 0) + ? (sortedValues[middleIndex - 1] + sortedValues[middleIndex]) / 2.0 + : sortedValues[middleIndex]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -92,15 +136,15 @@ public class Median : AbstractBase double median; if (_index >= Period) { - // Get sorted copy of values - var sortedValues = _buffer.GetSpan().ToArray(); - Array.Sort(sortedValues); - int middleIndex = sortedValues.Length / 2; + // Create a temporary buffer on the stack + Span values = stackalloc double[Period]; + _buffer.GetSpan().CopyTo(values); + + // Sort values in-place + QuickSort(values, 0, values.Length - 1); // Calculate median based on odd/even count - median = (sortedValues.Length % 2 == 0) - ? (sortedValues[middleIndex - 1] + sortedValues[middleIndex]) / 2.0 - : sortedValues[middleIndex]; + median = CalculateMedian(values); } else { diff --git a/lib/statistics/Min.cs b/lib/statistics/Min.cs index 2ebede8c..67b920eb 100644 --- a/lib/statistics/Min.cs +++ b/lib/statistics/Min.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -40,7 +40,8 @@ namespace QuanTAlib; /// Note: Decay factor allows for adaptive low tracking /// -public class Min : AbstractBase +[SkipLocalsInit] +public sealed class Min : AbstractBase { private readonly int Period; private readonly CircularBuffer _buffer; @@ -49,11 +50,15 @@ public class Min : AbstractBase private double _p_currentMin; private int _timeSinceNewMin; private int _p_timeSinceNewMin; + private const double DefaultDecay = 0.0; + private const double DecayScaleFactor = 0.1; + private const double Epsilon = 1e-10; /// The number of points to consider for minimum calculation. /// Half-life decay factor (0 for no decay, higher for faster forgetting). /// Thrown when period is less than 1 or decay is negative. - public Min(int period, double decay = 0) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Min(int period, double decay = DefaultDecay) { if (period < 1) { @@ -66,7 +71,7 @@ public class Min : AbstractBase Period = period; WarmupPeriod = 0; _buffer = new CircularBuffer(period); - _halfLife = decay * 0.1; + _halfLife = decay * DecayScaleFactor; Name = $"Min(period={period}, halfLife={decay:F2})"; Init(); } @@ -74,12 +79,14 @@ public class Min : AbstractBase /// The data source object that publishes updates. /// The number of points to consider for minimum calculation. /// Half-life decay factor (default 0). - public Min(object source, int period, double decay = 0) : this(period, decay) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Min(object source, int period, double decay = DefaultDecay) : this(period, decay) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -87,6 +94,7 @@ public class Min : AbstractBase _timeSinceNewMin = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -104,6 +112,27 @@ public class Min : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private double CalculateDecayRate() + { + return 1 - Math.Exp(-_halfLife * _timeSinceNewMin / Period); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double FindMinValue(ReadOnlySpan values) + { + double min = double.MaxValue; + for (int i = 0; i < values.Length; i++) + { + if (values[i] < min) + { + min = values[i]; + } + } + return min; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -117,11 +146,12 @@ public class Min : AbstractBase } // Apply decay based on time since last minimum - double decayRate = 1 - Math.Exp(-_halfLife * _timeSinceNewMin / Period); + double decayRate = CalculateDecayRate(); _currentMin += decayRate * (_buffer.Average() - _currentMin); // Ensure minimum doesn't fall below current period's lowest value - _currentMin = Math.Max(_currentMin, _buffer.Min()); + ReadOnlySpan values = _buffer.GetSpan(); + _currentMin = Math.Max(_currentMin, FindMinValue(values)); IsHot = true; return _currentMin; diff --git a/lib/statistics/Mode.cs b/lib/statistics/Mode.cs index 91629638..26069517 100644 --- a/lib/statistics/Mode.cs +++ b/lib/statistics/Mode.cs @@ -1,5 +1,5 @@ -using System; -using System.Linq; +using System.Collections.Generic; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -40,13 +40,18 @@ namespace QuanTAlib; /// Note: Particularly useful for price level analysis /// -public class Mode : AbstractBase +[SkipLocalsInit] +public sealed class Mode : AbstractBase { private readonly int Period; private readonly CircularBuffer _buffer; + private readonly Dictionary _frequencies; + private readonly List _modes; + private const double Epsilon = 1e-10; /// The number of points to consider for mode calculation. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mode(int period) { if (period < 1) @@ -56,24 +61,31 @@ public class Mode : AbstractBase Period = period; WarmupPeriod = period; _buffer = new CircularBuffer(period); + _frequencies = new Dictionary(); + _modes = new List(); Name = $"Mode(period={period})"; Init(); } /// The data source object that publishes updates. /// The number of points to consider for mode calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Mode(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _buffer.Clear(); + _frequencies.Clear(); + _modes.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -83,6 +95,49 @@ public class Mode : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void CountFrequencies(ReadOnlySpan values) + { + _frequencies.Clear(); + for (int i = 0; i < values.Length; i++) + { + _frequencies[values[i]] = _frequencies.TryGetValue(values[i], out int count) ? count + 1 : 1; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void FindModes() + { + _modes.Clear(); + int maxCount = 0; + + foreach (var kvp in _frequencies) + { + if (kvp.Value > maxCount) + { + maxCount = kvp.Value; + _modes.Clear(); + _modes.Add(kvp.Key); + } + else if (kvp.Value == maxCount) + { + _modes.Add(kvp.Key); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private double CalculateAverageMode() + { + double sum = 0; + for (int i = 0; i < _modes.Count; i++) + { + sum += _modes[i]; + } + return sum / _modes.Count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -91,21 +146,10 @@ public class Mode : AbstractBase double mode; if (_index >= Period) { - // Group values by frequency and order by count - var values = _buffer.GetSpan().ToArray(); - var groupedValues = values.GroupBy(v => v) - .OrderByDescending(g => g.Count()) - .ThenBy(g => g.Key) - .ToList(); - - // Find all values with highest frequency - int maxCount = groupedValues.First().Count(); - var modes = groupedValues.TakeWhile(g => g.Count() == maxCount) - .Select(g => g.Key) - .ToList(); - - // Average multiple modes if present - mode = modes.Average(); + ReadOnlySpan values = _buffer.GetSpan(); + CountFrequencies(values); + FindModes(); + mode = CalculateAverageMode(); } else { diff --git a/lib/statistics/Percentile.cs b/lib/statistics/Percentile.cs index 4978a6ae..4617cdc0 100644 --- a/lib/statistics/Percentile.cs +++ b/lib/statistics/Percentile.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -41,20 +40,24 @@ namespace QuanTAlib; /// Note: Particularly useful for risk metrics like VaR /// -public class Percentile : AbstractBase +[SkipLocalsInit] +public sealed class Percentile : AbstractBase { private readonly int Period; private readonly double Percent; private readonly CircularBuffer _buffer; + private const double Epsilon = 1e-10; + private const int MinimumPoints = 2; /// The number of points to consider for percentile calculation. /// The percentile to calculate (0-100). /// /// Thrown when period is less than 2 or percent is not between 0 and 100. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Percentile(int period, double percent) { - if (period < 2) + if (period < MinimumPoints) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2 for percentile calculation."); @@ -66,7 +69,7 @@ public class Percentile : AbstractBase } Period = period; Percent = percent; - WarmupPeriod = 2; // Minimum number of points needed for percentile calculation + WarmupPeriod = MinimumPoints; // Minimum number of points needed for percentile calculation _buffer = new CircularBuffer(period); Name = $"Percentile(period={period}, percent={percent})"; Init(); @@ -75,18 +78,21 @@ public class Percentile : AbstractBase /// The data source object that publishes updates. /// The number of points to consider for percentile calculation. /// The percentile to calculate (0-100). + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Percentile(object source, int period, double percent) : this(period, percent) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _buffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -96,6 +102,56 @@ public class Percentile : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static void QuickSort(Span arr, int left, int right) + { + if (left < right) + { + int pivotIndex = Partition(arr, left, right); + QuickSort(arr, left, pivotIndex - 1); + QuickSort(arr, pivotIndex + 1, right); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static int Partition(Span arr, int left, int right) + { + double pivot = arr[right]; + int i = left - 1; + + for (int j = left; j < right; j++) + { + if (arr[j] <= pivot) + { + i++; + (arr[i], arr[j]) = (arr[j], arr[i]); + } + } + + (arr[i + 1], arr[right]) = (arr[right], arr[i + 1]); + return i + 1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private double CalculatePercentile(Span sortedValues) + { + double position = (Percent / 100.0) * (sortedValues.Length - 1); + int lowerIndex = (int)Math.Floor(position); + int upperIndex = (int)Math.Ceiling(position); + + if (lowerIndex == upperIndex) + { + return sortedValues[lowerIndex]; + } + + // Linear interpolation between adjacent values + double lowerValue = sortedValues[lowerIndex]; + double upperValue = sortedValues[upperIndex]; + double fraction = position - lowerIndex; + return lowerValue + (upperValue - lowerValue) * fraction; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -104,26 +160,12 @@ public class Percentile : AbstractBase double result; if (_buffer.Count >= Period) { - // Sort values and calculate percentile position - var values = _buffer.GetSpan().ToArray(); - Array.Sort(values); + // Create a temporary buffer on the stack and sort values + Span values = stackalloc double[Period]; + _buffer.GetSpan().CopyTo(values); + QuickSort(values, 0, values.Length - 1); - double position = (Percent / 100.0) * (values.Length - 1); - int lowerIndex = (int)Math.Floor(position); - int upperIndex = (int)Math.Ceiling(position); - - if (lowerIndex == upperIndex) - { - result = values[lowerIndex]; - } - else - { - // Linear interpolation between adjacent values - double lowerValue = values[lowerIndex]; - double upperValue = values[upperIndex]; - double fraction = position - lowerIndex; - result = lowerValue + (upperValue - lowerValue) * fraction; - } + result = CalculatePercentile(values); } else { diff --git a/lib/statistics/Skew.cs b/lib/statistics/Skew.cs index 3ab7db22..65f9613f 100644 --- a/lib/statistics/Skew.cs +++ b/lib/statistics/Skew.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -44,22 +43,26 @@ namespace QuanTAlib; /// Note: Requires minimum of 3 data points for calculation /// -public class Skew : AbstractBase +[SkipLocalsInit] +public sealed class Skew : AbstractBase { private readonly int Period; private readonly CircularBuffer _buffer; + private const double Epsilon = 1e-10; + private const int MinimumPoints = 3; /// The number of points to consider for skewness calculation. /// Thrown when period is less than 3. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Skew(int period) { - if (period < 3) + if (period < MinimumPoints) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 3 for skewness calculation."); } Period = period; - WarmupPeriod = 3; + WarmupPeriod = MinimumPoints; _buffer = new CircularBuffer(period); Name = $"Skew(period={period})"; Init(); @@ -67,18 +70,21 @@ public class Skew : AbstractBase /// The data source object that publishes updates. /// The number of points to consider for skewness calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Skew(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _buffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -88,38 +94,58 @@ public class Skew : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateMean(ReadOnlySpan values) + { + double sum = 0; + for (int i = 0; i < values.Length; i++) + { + sum += values[i]; + } + return sum / values.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double m3, double m2) CalculateMoments(ReadOnlySpan values, double mean) + { + double sumCubedDeviations = 0; + double sumSquaredDeviations = 0; + + for (int i = 0; i < values.Length; i++) + { + double deviation = values[i] - mean; + double squared = deviation * deviation; + sumSquaredDeviations += squared; + sumCubedDeviations += squared * deviation; + } + + double n = values.Length; + return (sumCubedDeviations / n, sumSquaredDeviations / n); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateSkewness(double m3, double m2, int n) + { + double s3 = Math.Pow(m2, 1.5); + if (s3 < Epsilon) + return 0; + + return (Math.Sqrt(n * (n - 1)) / (n - 2)) * (m3 / s3); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); _buffer.Add(Input.Value, Input.IsNew); double skew = 0; - if (_buffer.Count >= 3) // Need at least 3 points for skewness + if (_buffer.Count >= MinimumPoints) // Need at least 3 points for skewness { - var values = _buffer.GetSpan().ToArray(); - double mean = values.Average(); - double n = values.Length; - - // Calculate third and second moments - double sumCubedDeviations = 0; - double sumSquaredDeviations = 0; - - foreach (var value in values) - { - double deviation = value - mean; - sumCubedDeviations += Math.Pow(deviation, 3); - sumSquaredDeviations += Math.Pow(deviation, 2); - } - - // Fisher-Pearson standardized moment coefficient - double m3 = sumCubedDeviations / n; - double m2 = sumSquaredDeviations / n; - double s3 = Math.Pow(m2, 1.5); - - if (s3 != 0) // Avoid division by zero - { - skew = (Math.Sqrt(n * (n - 1)) / (n - 2)) * (m3 / s3); - } + ReadOnlySpan values = _buffer.GetSpan(); + double mean = CalculateMean(values); + var (m3, m2) = CalculateMoments(values, mean); + skew = CalculateSkewness(m3, m2, values.Length); } IsHot = _buffer.Count >= Period; diff --git a/lib/statistics/Slope.cs b/lib/statistics/Slope.cs index 8731e507..df9adf15 100644 --- a/lib/statistics/Slope.cs +++ b/lib/statistics/Slope.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -43,11 +42,14 @@ namespace QuanTAlib; /// Note: Provides additional regression statistics (R², intercept) /// -public class Slope : AbstractBase +[SkipLocalsInit] +public sealed class Slope : AbstractBase { private readonly int _period; private readonly CircularBuffer _buffer; private readonly CircularBuffer _timeBuffer; + private const double Epsilon = 1e-10; + private const int MinimumPoints = 2; /// Gets the y-intercept of the regression line. public double? Intercept { get; private set; } @@ -63,6 +65,7 @@ public class Slope : AbstractBase /// The number of points to consider for slope calculation. /// Thrown when period is less than or equal to 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Slope(int period) { if (period <= 1) @@ -80,12 +83,14 @@ public class Slope : AbstractBase /// The data source object that publishes updates. /// The number of points to consider for slope calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Slope(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -97,6 +102,7 @@ public class Slope : AbstractBase Line = null; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -106,33 +112,22 @@ public class Slope : AbstractBase } } - protected override double Calculation() + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double sumX, double sumY) CalculateSums(ReadOnlySpan values, int count) { - ManageState(Input.IsNew); - - _buffer.Add(Input.Value, Input.IsNew); - _timeBuffer.Add(Input.Time.Ticks, Input.IsNew); - - double slope = 0; - if (_buffer.Count < 2) - { - return slope; // Need at least 2 points - } - - int count = Math.Min(_buffer.Count, _period); - var values = _buffer.GetSpan().ToArray(); - - // Calculate averages double sumX = 0, sumY = 0; for (int i = 0; i < count; i++) { sumX += i + 1; sumY += values[i]; } - double avgX = sumX / count; - double avgY = sumY / count; + return (sumX, sumY); + } - // Least squares regression + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double sumSqX, double sumSqY, double sumSqXY) CalculateSquaredSums( + ReadOnlySpan values, int count, double avgX, double avgY) + { double sumSqX = 0, sumSqY = 0, sumSqXY = 0; for (int i = 0; i < count; i++) { @@ -142,8 +137,35 @@ public class Slope : AbstractBase sumSqY += devY * devY; sumSqXY += devX * devY; } + return (sumSqX, sumSqY, sumSqXY); + } - if (sumSqX > 0) + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + _buffer.Add(Input.Value, Input.IsNew); + _timeBuffer.Add(Input.Time.Ticks, Input.IsNew); + + double slope = 0; + if (_buffer.Count < MinimumPoints) + { + return slope; // Need at least 2 points + } + + int count = Math.Min(_buffer.Count, _period); + ReadOnlySpan values = _buffer.GetSpan(); + + // Calculate averages + var (sumX, sumY) = CalculateSums(values, count); + double avgX = sumX / count; + double avgY = sumY / count; + + // Least squares regression + var (sumSqX, sumSqY, sumSqXY) = CalculateSquaredSums(values, count, avgX, avgY); + + if (sumSqX > Epsilon) { // Calculate slope and related statistics slope = sumSqXY / sumSqX; @@ -154,9 +176,10 @@ public class Slope : AbstractBase double stdDevY = Math.Sqrt(sumSqY / count); StdDev = stdDevY; - if (stdDevX * stdDevY != 0) + double stdDevProduct = stdDevX * stdDevY; + if (stdDevProduct > Epsilon) { - double r = sumSqXY / (stdDevX * stdDevY) / count; + double r = sumSqXY / stdDevProduct / count; RSquared = r * r; } diff --git a/lib/statistics/Stddev.cs b/lib/statistics/Stddev.cs index 47433a91..4504a58f 100644 --- a/lib/statistics/Stddev.cs +++ b/lib/statistics/Stddev.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -44,17 +43,21 @@ namespace QuanTAlib; /// Note: Foundation for many volatility-based indicators /// -public class Stddev : AbstractBase +[SkipLocalsInit] +public sealed class Stddev : AbstractBase { private readonly bool IsPopulation; private readonly CircularBuffer _buffer; + private const double Epsilon = 1e-10; + private const int MinimumPoints = 2; /// The number of points to consider for standard deviation calculation. /// True for population stddev, false for sample stddev (default). /// Thrown when period is less than 2. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Stddev(int period, bool isPopulation = false) { - if (period < 2) + if (period < MinimumPoints) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2."); @@ -69,18 +72,21 @@ public class Stddev : AbstractBase /// The data source object that publishes updates. /// The number of points to consider for standard deviation calculation. /// True for population stddev, false for sample stddev (default). + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Stddev(object source, int period, bool isPopulation = false) : this(period, isPopulation) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _buffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -90,6 +96,30 @@ public class Stddev : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateMean(ReadOnlySpan values) + { + double sum = 0; + for (int i = 0; i < values.Length; i++) + { + sum += values[i]; + } + return sum / values.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateSumSquaredDeviations(ReadOnlySpan values, double mean) + { + double sum = 0; + for (int i = 0; i < values.Length; i++) + { + double diff = values[i] - mean; + sum += diff * diff; + } + return sum; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -98,11 +128,9 @@ public class Stddev : AbstractBase double stddev = 0; if (_buffer.Count > 1) { - var values = _buffer.GetSpan().ToArray(); - double mean = values.Average(); - - // Calculate sum of squared deviations - double sumOfSquaredDifferences = values.Sum(x => Math.Pow(x - mean, 2)); + ReadOnlySpan values = _buffer.GetSpan(); + double mean = CalculateMean(values); + double sumOfSquaredDifferences = CalculateSumSquaredDeviations(values, mean); // Use appropriate divisor based on population/sample calculation double divisor = IsPopulation ? _buffer.Count : _buffer.Count - 1; diff --git a/lib/statistics/Variance.cs b/lib/statistics/Variance.cs index 12916507..a7c30a9f 100644 --- a/lib/statistics/Variance.cs +++ b/lib/statistics/Variance.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -44,17 +43,21 @@ namespace QuanTAlib; /// Note: Basis for Modern Portfolio Theory and risk models /// -public class Variance : AbstractBase +[SkipLocalsInit] +public sealed class Variance : AbstractBase { private readonly bool IsPopulation; private readonly CircularBuffer _buffer; + private const double Epsilon = 1e-10; + private const int MinimumPoints = 2; /// The number of points to consider for variance calculation. /// True for population variance, false for sample variance (default). /// Thrown when period is less than 2. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Variance(int period, bool isPopulation = false) { - if (period < 2) + if (period < MinimumPoints) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2."); @@ -69,18 +72,21 @@ public class Variance : AbstractBase /// The data source object that publishes updates. /// The number of points to consider for variance calculation. /// True for population variance, false for sample variance (default). + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Variance(object source, int period, bool isPopulation = false) : this(period, isPopulation) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _buffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -90,6 +96,30 @@ public class Variance : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateMean(ReadOnlySpan values) + { + double sum = 0; + for (int i = 0; i < values.Length; i++) + { + sum += values[i]; + } + return sum / values.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateSumSquaredDeviations(ReadOnlySpan values, double mean) + { + double sum = 0; + for (int i = 0; i < values.Length; i++) + { + double diff = values[i] - mean; + sum += diff * diff; + } + return sum; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -98,11 +128,9 @@ public class Variance : AbstractBase double variance = 0; if (_buffer.Count > 1) { - var values = _buffer.GetSpan().ToArray(); - double mean = values.Average(); - - // Calculate sum of squared deviations - double sumOfSquaredDifferences = values.Sum(x => Math.Pow(x - mean, 2)); + ReadOnlySpan values = _buffer.GetSpan(); + double mean = CalculateMean(values); + double sumOfSquaredDifferences = CalculateSumSquaredDeviations(values, mean); // Use appropriate divisor based on population/sample calculation double divisor = IsPopulation ? _buffer.Count : _buffer.Count - 1; diff --git a/lib/statistics/Zscore.cs b/lib/statistics/Zscore.cs index c3c72769..752559fb 100644 --- a/lib/statistics/Zscore.cs +++ b/lib/statistics/Zscore.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -43,22 +42,26 @@ namespace QuanTAlib; /// Note: Assumes approximately normal distribution /// -public class Zscore : AbstractBase +[SkipLocalsInit] +public sealed class Zscore : AbstractBase { private readonly int Period; private readonly CircularBuffer _buffer; + private const double Epsilon = 1e-10; + private const int MinimumPoints = 2; /// The number of points to consider for Z-score calculation. /// Thrown when period is less than 2. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Zscore(int period) { - if (period < 2) + if (period < MinimumPoints) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2 for Z-score calculation."); } Period = period; - WarmupPeriod = 2; + WarmupPeriod = MinimumPoints; _buffer = new CircularBuffer(period); Name = $"ZScore(period={period})"; Init(); @@ -66,18 +69,21 @@ public class Zscore : AbstractBase /// The data source object that publishes updates. /// The number of points to consider for Z-score calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Zscore(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _buffer.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -87,23 +93,43 @@ public class Zscore : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateMean(ReadOnlySpan values) + { + double sum = 0; + for (int i = 0; i < values.Length; i++) + { + sum += values[i]; + } + return sum / values.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateStandardDeviation(ReadOnlySpan values, double mean) + { + double sumSquaredDeviations = 0; + for (int i = 0; i < values.Length; i++) + { + double deviation = values[i] - mean; + sumSquaredDeviations += deviation * deviation; + } + return Math.Sqrt(sumSquaredDeviations / (values.Length - 1)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); _buffer.Add(Input.Value, Input.IsNew); double zScore = 0; - if (_buffer.Count >= 2) // Need at least 2 points for standard deviation + if (_buffer.Count >= MinimumPoints) // Need at least 2 points for standard deviation { - var values = _buffer.GetSpan().ToArray(); - double mean = values.Average(); - double n = values.Length; + ReadOnlySpan values = _buffer.GetSpan(); + double mean = CalculateMean(values); + double standardDeviation = CalculateStandardDeviation(values, mean); - // Calculate sample standard deviation - double sumSquaredDeviations = values.Sum(x => Math.Pow(x - mean, 2)); - double standardDeviation = Math.Sqrt(sumSquaredDeviations / (n - 1)); - - if (standardDeviation != 0) // Avoid division by zero + if (standardDeviation > Epsilon) // Avoid division by zero { zScore = (Input.Value - mean) / standardDeviation; } diff --git a/lib/volatility/Atr.cs b/lib/volatility/Atr.cs index 43d1983a..c098e83f 100644 --- a/lib/volatility/Atr.cs +++ b/lib/volatility/Atr.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -42,7 +42,8 @@ namespace QuanTAlib; /// Note: Higher ATR indicates higher volatility /// -public class Atr : AbstractBase +[SkipLocalsInit] +public sealed class Atr : AbstractBase { public double Tr { get; private set; } private readonly Rma _ma; @@ -50,6 +51,7 @@ public class Atr : AbstractBase /// The number of periods for ATR calculation. /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Atr(int period) { if (period < 1) @@ -64,12 +66,14 @@ public class Atr : AbstractBase /// The data source object that publishes updates. /// The number of periods for ATR calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Atr(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new BarSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -78,6 +82,7 @@ public class Atr : AbstractBase Tr = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -91,6 +96,17 @@ public class Atr : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateTrueRange(double high, double low, double prevClose) + { + double highLowRange = high - low; + double highPrevCloseRange = Math.Abs(high - prevClose); + double lowPrevCloseRange = Math.Abs(low - prevClose); + + return Math.Max(highLowRange, Math.Max(highPrevCloseRange, lowPrevCloseRange)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(BarInput.IsNew); @@ -104,13 +120,7 @@ public class Atr : AbstractBase else { // Calculate True Range as maximum of three measures - Tr = Math.Max( - BarInput.High - BarInput.Low, - Math.Max( - Math.Abs(BarInput.High - _prevClose), - Math.Abs(BarInput.Low - _prevClose) - ) - ); + Tr = CalculateTrueRange(BarInput.High, BarInput.Low, _prevClose); } // Apply RMA smoothing to True Range diff --git a/lib/volatility/Hv.cs b/lib/volatility/Hv.cs index f6c73cc7..fb246cce 100644 --- a/lib/volatility/Hv.cs +++ b/lib/volatility/Hv.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -44,17 +43,21 @@ namespace QuanTAlib; /// Note: Assumes 252 trading days for annualization /// -public class Hv : AbstractBase +[SkipLocalsInit] +public sealed class Hv : AbstractBase { private readonly int Period; private readonly bool IsAnnualized; private readonly CircularBuffer _buffer; private readonly CircularBuffer _logReturns; private double _previousClose; + private const int TradingDaysPerYear = 252; + private const double Epsilon = 1e-10; /// The number of periods for volatility calculation. /// Whether to annualize the result (default true). /// Thrown when period is less than 2. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Hv(int period, bool isAnnualized = true) { if (period < 2) @@ -74,12 +77,14 @@ public class Hv : AbstractBase /// The data source object that publishes updates. /// The number of periods for volatility calculation. /// Whether to annualize the result (default true). + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Hv(object source, int period, bool isAnnualized = true) : this(period, isAnnualized) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -88,6 +93,7 @@ public class Hv : AbstractBase _previousClose = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -97,6 +103,36 @@ public class Hv : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateLogReturn(double currentPrice, double previousPrice) + { + return previousPrice > Epsilon ? Math.Log(currentPrice / previousPrice) : 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateMean(ReadOnlySpan values) + { + double sum = 0; + for (int i = 0; i < values.Length; i++) + { + sum += values[i]; + } + return sum / values.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateVariance(ReadOnlySpan values, double mean, int degreesOfFreedom) + { + double sumSquaredDiff = 0; + for (int i = 0; i < values.Length; i++) + { + double diff = values[i] - mean; + sumSquaredDiff += diff * diff; + } + return sumSquaredDiff / degreesOfFreedom; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -106,26 +142,23 @@ public class Hv : AbstractBase if (_buffer.Count > 1) { // Calculate log return if we have previous close - if (_previousClose != 0) + if (_previousClose > Epsilon) { - double logReturn = Math.Log(Input.Value / _previousClose); + double logReturn = CalculateLogReturn(Input.Value, _previousClose); _logReturns.Add(logReturn, Input.IsNew); } // Calculate volatility when we have enough returns if (_logReturns.Count == Period) { - var returns = _logReturns.GetSpan().ToArray(); - double mean = returns.Average(); - double sumOfSquaredDifferences = returns.Sum(x => Math.Pow(x - mean, 2)); - - // Sample standard deviation - double variance = sumOfSquaredDifferences / (Period - 1); + ReadOnlySpan returns = _logReturns.GetSpan(); + double mean = CalculateMean(returns); + double variance = CalculateVariance(returns, mean, Period - 1); volatility = Math.Sqrt(variance); if (IsAnnualized) { - volatility *= Math.Sqrt(252); // Annualize using trading days + volatility *= Math.Sqrt(TradingDaysPerYear); } } } diff --git a/lib/volatility/Jvolty.cs b/lib/volatility/Jvolty.cs index af7c305e..424bc8cb 100644 --- a/lib/volatility/Jvolty.cs +++ b/lib/volatility/Jvolty.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -41,31 +41,37 @@ namespace QuanTAlib; /// Note: Proprietary enhancement of volatility measurement /// -public class Jvolty : AbstractBase +[SkipLocalsInit] +public sealed class Jvolty : AbstractBase { private readonly int _period; private readonly double _phase; private readonly CircularBuffer _vsumBuff; private readonly CircularBuffer _avoltyBuff; + private readonly double _beta; + private const double Epsilon = 1e-10; + private const int DefaultPhase = 0; + private const int VsumBufferSize = 10; + private const int AvoltyBufferSize = 65; private double _len1; private double _pow1; - private readonly double _beta; private double _upperBand, _lowerBand, _p_upperBand, _p_lowerBand; private double _prevMa1, _prevDet0, _prevDet1, _prevJma, _p_prevMa1, _p_prevDet0, _p_prevDet1, _p_prevJma; private double _vSum, _p_vSum; - public double UpperBand { get; set; } - public double LowerBand { get; set; } - public double Volty { get; set; } - public double VSum { get; set; } - public double Jma { get; set; } - public double AvgVolty { get; set; } + public double UpperBand { get; private set; } + public double LowerBand { get; private set; } + public double Volty { get; private set; } + public double VSum { get; private set; } + public double Jma { get; private set; } + public double AvgVolty { get; private set; } /// The number of periods for volatility calculation. /// Phase parameter for JMA smoothing (default 0). /// Thrown when period is less than 1. - public Jvolty(int period, int phase = 0) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Jvolty(int period, int phase = DefaultPhase) { if (period < 1) { @@ -75,8 +81,8 @@ public class Jvolty : AbstractBase _period = period; _phase = Math.Clamp((phase * 0.01) + 1.5, 0.5, 2.5); - _vsumBuff = new CircularBuffer(10); - _avoltyBuff = new CircularBuffer(65); + _vsumBuff = new CircularBuffer(VsumBufferSize); + _avoltyBuff = new CircularBuffer(AvoltyBufferSize); _beta = 0.45 * (period - 1) / (0.45 * (period - 1) + 2); WarmupPeriod = period * 2; @@ -86,12 +92,14 @@ public class Jvolty : AbstractBase /// The data source object that publishes updates. /// The number of periods for volatility calculation. /// Phase parameter for JMA smoothing (default 0). - public Jvolty(object source, int period, int phase = 0) : this(period, phase) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Jvolty(object source, int period, int phase = DefaultPhase) : this(period, phase) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new BarSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -103,6 +111,7 @@ public class Jvolty : AbstractBase _vsumBuff.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -128,6 +137,37 @@ public class Jvolty : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateVolatility(double price, double upperBand, double lowerBand) + { + double del1 = price - upperBand; + double del2 = price - lowerBand; + return Math.Max(Math.Abs(del1), Math.Abs(del2)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private double CalculateNormalizedVolatility(double volty, double avgVolty) + { + double rvolty = (avgVolty > Epsilon) ? volty / avgVolty : 1; + return Math.Min(Math.Max(rvolty, 1.0), Math.Pow(_len1, 1.0 / _pow1)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private double CalculateJma(double price, double alpha, double ma1) + { + double det0 = (price - ma1) * (1 - _beta) + _beta * _prevDet0; + _prevDet0 = det0; + double ma2 = ma1 + _phase * det0; + + double det1 = ((ma2 - _prevJma) * (1 - alpha) * (1 - alpha)) + (alpha * alpha * _prevDet1); + _prevDet1 = det1; + double jma = _prevJma + det1; + _prevJma = jma; + + return jma; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -139,40 +179,31 @@ public class Jvolty : AbstractBase } // Calculate volatility from band distances - double del1 = price - _upperBand; - double del2 = price - _lowerBand; - double volty = Math.Max(Math.Abs(del1), Math.Abs(del2)); + double volty = CalculateVolatility(price, _upperBand, _lowerBand); // Calculate moving averages of volatility _vsumBuff.Add(volty, Input.IsNew); - _vSum += (_vsumBuff[^1] - _vsumBuff[0]) / 10; + _vSum += (_vsumBuff[^1] - _vsumBuff[0]) / VsumBufferSize; _avoltyBuff.Add(_vSum, Input.IsNew); double avgvolty = _avoltyBuff.Average(); // Normalize and adjust volatility - double rvolty = (avgvolty > 0) ? volty / avgvolty : 1; - rvolty = Math.Min(Math.Max(rvolty, 1.0), Math.Pow(_len1, 1.0 / _pow1)); - + double rvolty = CalculateNormalizedVolatility(volty, avgvolty); double pow2 = Math.Pow(rvolty, _pow1); double Kv = Math.Pow(_beta, Math.Sqrt(pow2)); // Update adaptive bands + double del1 = price - _upperBand; + double del2 = price - _lowerBand; _upperBand = (del1 >= 0) ? price : price - (Kv * del1); _lowerBand = (del2 <= 0) ? price : price - (Kv * del2); // Apply JMA smoothing double alpha = Math.Pow(_beta, pow2); - double ma1 = (1 - alpha) * Input.Value + alpha * _prevMa1; + double ma1 = (1 - alpha) * price + alpha * _prevMa1; _prevMa1 = ma1; - double det0 = (price - ma1) * (1 - _beta) + _beta * _prevDet0; - _prevDet0 = det0; - double ma2 = ma1 + _phase * det0; - - double det1 = ((ma2 - _prevJma) * (1 - alpha) * (1 - alpha)) + (alpha * alpha * _prevDet1); - _prevDet1 = det1; - double jma = _prevJma + det1; - _prevJma = jma; + double jma = CalculateJma(price, alpha, ma1); // Update public properties UpperBand = _upperBand; diff --git a/lib/volatility/Rv.cs b/lib/volatility/Rv.cs index b171a173..d53615c4 100644 --- a/lib/volatility/Rv.cs +++ b/lib/volatility/Rv.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -43,18 +43,23 @@ namespace QuanTAlib; /// Note: Efficient implementation using rolling sums /// -public class Rv : AbstractBase +[SkipLocalsInit] +public sealed class Rv : AbstractBase { private readonly int Period; private readonly bool IsAnnualized; private readonly CircularBuffer _returns; private double _previousClose; private double _sumSquaredReturns; + private const int TradingDaysPerYear = 252; + private const double Epsilon = 1e-10; + private const bool DefaultIsAnnualized = true; /// The number of periods for volatility calculation. /// Whether to annualize the result (default true). /// Thrown when period is less than 2. - public Rv(int period, bool isAnnualized = true) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rv(int period, bool isAnnualized = DefaultIsAnnualized) { if (period < 2) { @@ -72,12 +77,14 @@ public class Rv : AbstractBase /// The data source object that publishes updates. /// The number of periods for volatility calculation. /// Whether to annualize the result (default true). - public Rv(object source, int period, bool isAnnualized = true) : this(period, isAnnualized) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rv(object source, int period, bool isAnnualized = DefaultIsAnnualized) : this(period, isAnnualized) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); @@ -86,6 +93,7 @@ public class Rv : AbstractBase _sumSquaredReturns = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -95,36 +103,46 @@ public class Rv : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateLogReturn(double currentPrice, double previousPrice) + { + return previousPrice > Epsilon ? Math.Log(currentPrice / previousPrice) : 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateVolatility(double sumSquaredReturns, int period, bool isAnnualized) + { + double variance = sumSquaredReturns / period; + double volatility = Math.Sqrt(variance); + return isAnnualized ? volatility * Math.Sqrt(TradingDaysPerYear) : volatility; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); double volatility = 0; - if (_previousClose != 0) + if (_previousClose > Epsilon) { // Calculate log return - double logReturn = Math.Log(Input.Value / _previousClose); + double logReturn = CalculateLogReturn(Input.Value, _previousClose); if (_returns.Count == Period) { // Maintain rolling sum by removing oldest squared return - _sumSquaredReturns -= Math.Pow(_returns[0], 2); + double oldReturn = _returns[0]; + _sumSquaredReturns -= oldReturn * oldReturn; } // Add new return and update sum _returns.Add(logReturn, Input.IsNew); - _sumSquaredReturns += Math.Pow(logReturn, 2); + _sumSquaredReturns += logReturn * logReturn; if (_returns.Count == Period) { // Calculate realized volatility - double variance = _sumSquaredReturns / Period; - volatility = Math.Sqrt(variance); - - if (IsAnnualized) - { - volatility *= Math.Sqrt(252); // Annualize using trading days - } + volatility = CalculateVolatility(_sumSquaredReturns, Period, IsAnnualized); } } diff --git a/lib/volatility/Rvi.cs b/lib/volatility/Rvi.cs index f6468816..555727fb 100644 --- a/lib/volatility/Rvi.cs +++ b/lib/volatility/Rvi.cs @@ -1,4 +1,4 @@ -using System; +using System.Runtime.CompilerServices; namespace QuanTAlib; /// @@ -42,14 +42,18 @@ namespace QuanTAlib; /// Note: Similar concept to RSI but using volatility /// -public class Rvi : AbstractBase +[SkipLocalsInit] +public sealed class Rvi : AbstractBase { private readonly Stddev _upStdDev, _downStdDev; private readonly Sma _upSma, _downSma; private double _previousClose; + private const double ScalingFactor = 100.0; + private const double Epsilon = 1e-10; /// The number of periods for RVI calculation. /// Thrown when period is less than 2. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rvi(int period) { if (period < 2) @@ -57,30 +61,32 @@ public class Rvi : AbstractBase throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2."); } - int Period = period; WarmupPeriod = period; Name = $"RVI(period={period})"; - _upStdDev = new Stddev(Period); - _downStdDev = new Stddev(Period); - _upSma = new(Period); - _downSma = new(Period); + _upStdDev = new Stddev(period); + _downStdDev = new Stddev(period); + _upSma = new(period); + _downSma = new(period); Init(); } /// The data source object that publishes updates. /// The number of periods for RVI calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Rvi(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _previousClose = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) @@ -90,6 +96,20 @@ public class Rvi : AbstractBase } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double upMove, double downMove) CalculateMoves(double change) + { + return (Math.Max(change, 0), Math.Max(-change, 0)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateRvi(double upSma, double downSma) + { + double totalSma = upSma + downSma; + return totalSma > Epsilon ? ScalingFactor * upSma / totalSma : 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); @@ -98,18 +118,14 @@ public class Rvi : AbstractBase double change = close - _previousClose; // Separate into up and down moves - double upMove = Math.Max(change, 0); - double downMove = Math.Max(-change, 0); + var (upMove, downMove) = CalculateMoves(change); // Calculate standard deviations and apply smoothing _upSma.Calc(_upStdDev.Calc(new TValue(Input.Time, upMove, Input.IsNew))); _downSma.Calc(_downStdDev.Calc(new TValue(Input.Time, downMove, Input.IsNew))); // Calculate RVI ratio - double rvi; - rvi = (_upSma.Value + _downSma.Value != 0) - ? 100 * _upSma.Value / (_upSma.Value + _downSma.Value) - : 0; + double rvi = CalculateRvi(_upSma.Value, _downSma.Value); _previousClose = close; IsHot = _index >= WarmupPeriod;