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;