Class optimization

This commit is contained in:
Miha
2024-10-27 16:11:08 -07:00
parent b2fcdda785
commit 6c67a0cf31
77 changed files with 2634 additions and 1455 deletions
+44 -22
View File
@@ -1,5 +1,4 @@
using System;
using System.Linq;
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
@@ -36,11 +35,13 @@ namespace QuanTAlib;
/// Note: Second-order derivative providing acceleration insights
/// </remarks>
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;
/// <summary>
/// Gets the y-intercept of the curvature line.
@@ -64,6 +65,7 @@ public class Curvature : AbstractBase
/// <param name="period">The number of points to consider for calculation.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is 2 or less.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Curvature(int period)
{
if (period <= 2)
@@ -82,12 +84,14 @@ public class Curvature : AbstractBase
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The number of points to consider for calculation.</param>
[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<double> 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<double> 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<double> 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;
}
+51 -29
View File
@@ -1,5 +1,5 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
@@ -41,41 +41,52 @@ namespace QuanTAlib;
/// Note: Normalized to [0,1] for easier interpretation
/// </remarks>
public class Entropy : AbstractBase
[SkipLocalsInit]
public sealed class Entropy : AbstractBase
{
private readonly int Period;
private readonly CircularBuffer _buffer;
private readonly Dictionary<double, int> _valueCounts;
private const double Epsilon = 1e-10;
private const double DefaultEntropy = 1.0;
private const int MinimumPoints = 2;
/// <param name="period">The number of points to consider for entropy calculation.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than 2.</exception>
[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<double, int>();
Name = $"Entropy(period={period})";
Init();
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The number of points to consider for entropy calculation.</param>
[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<double> values, Dictionary<double, int> 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<double, int> 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<double> 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;
}
+57 -25
View File
@@ -1,5 +1,4 @@
using System;
using System.Linq;
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
@@ -42,16 +41,20 @@ namespace QuanTAlib;
/// Note: Returns excess kurtosis (normal distribution = 0)
/// </remarks>
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;
/// <param name="period">The number of points to consider for kurtosis calculation.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than 4.</exception>
[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
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The number of points to consider for kurtosis calculation.</param>
[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<double> 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<double> 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<double> values = _buffer.GetSpan();
double mean = CalculateMean(values);
var (s2, s4) = CalculateDeviations(values, mean);
kurtosis = CalculateSheskinKurtosis(s2, s4, values.Length);
}
IsHot = _buffer.Count >= Period;
+37 -7
View File
@@ -1,4 +1,4 @@
using System;
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
@@ -40,7 +40,8 @@ namespace QuanTAlib;
/// Note: Decay factor allows for adaptive peak tracking
/// </remarks>
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;
/// <param name="period">The number of points to consider for maximum calculation.</param>
/// <param name="decay">Half-life decay factor (0 for no decay, higher for faster forgetting).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than 1 or decay is negative.</exception>
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
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The number of points to consider for maximum calculation.</param>
/// <param name="decay">Half-life decay factor (default 0).</param>
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<double> 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<double> values = _buffer.GetSpan();
_currentMax = Math.Min(_currentMax, FindMaxValue(values));
IsHot = true;
return _currentMax;
+54 -10
View File
@@ -1,5 +1,4 @@
using System;
using System.Linq;
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
@@ -40,13 +39,15 @@ namespace QuanTAlib;
/// Note: More robust than mean for non-normal distributions
/// </remarks>
public class Median : AbstractBase
[SkipLocalsInit]
public sealed class Median : AbstractBase
{
private readonly int Period;
private readonly CircularBuffer _buffer;
/// <param name="period">The number of points to consider for median calculation.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than 1.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Median(int period)
{
if (period < 1)
@@ -63,18 +64,21 @@ public class Median : AbstractBase
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The number of points to consider for median calculation.</param>
[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<double> 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<double> 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<double> 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<double> 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
{
+37 -7
View File
@@ -1,4 +1,4 @@
using System;
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
@@ -40,7 +40,8 @@ namespace QuanTAlib;
/// Note: Decay factor allows for adaptive low tracking
/// </remarks>
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;
/// <param name="period">The number of points to consider for minimum calculation.</param>
/// <param name="decay">Half-life decay factor (0 for no decay, higher for faster forgetting).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than 1 or decay is negative.</exception>
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
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The number of points to consider for minimum calculation.</param>
/// <param name="decay">Half-life decay factor (default 0).</param>
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<double> 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<double> values = _buffer.GetSpan();
_currentMin = Math.Max(_currentMin, FindMinValue(values));
IsHot = true;
return _currentMin;
+62 -18
View File
@@ -1,5 +1,5 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
@@ -40,13 +40,18 @@ namespace QuanTAlib;
/// Note: Particularly useful for price level analysis
/// </remarks>
public class Mode : AbstractBase
[SkipLocalsInit]
public sealed class Mode : AbstractBase
{
private readonly int Period;
private readonly CircularBuffer _buffer;
private readonly Dictionary<double, int> _frequencies;
private readonly List<double> _modes;
private const double Epsilon = 1e-10;
/// <param name="period">The number of points to consider for mode calculation.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than 1.</exception>
[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<double, int>();
_modes = new List<double>();
Name = $"Mode(period={period})";
Init();
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The number of points to consider for mode calculation.</param>
[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<double> 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<double> values = _buffer.GetSpan();
CountFrequencies(values);
FindModes();
mode = CalculateAverageMode();
}
else
{
+66 -24
View File
@@ -1,5 +1,4 @@
using System;
using System.Linq;
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
@@ -41,20 +40,24 @@ namespace QuanTAlib;
/// Note: Particularly useful for risk metrics like VaR
/// </remarks>
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;
/// <param name="period">The number of points to consider for percentile calculation.</param>
/// <param name="percent">The percentile to calculate (0-100).</param>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when period is less than 2 or percent is not between 0 and 100.
/// </exception>
[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
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The number of points to consider for percentile calculation.</param>
/// <param name="percent">The percentile to calculate (0-100).</param>
[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<double> 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<double> 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<double> 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<double> 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
{
+56 -30
View File
@@ -1,5 +1,4 @@
using System;
using System.Linq;
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
@@ -44,22 +43,26 @@ namespace QuanTAlib;
/// Note: Requires minimum of 3 data points for calculation
/// </remarks>
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;
/// <param name="period">The number of points to consider for skewness calculation.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than 3.</exception>
[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
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The number of points to consider for skewness calculation.</param>
[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<double> 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<double> 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<double> values = _buffer.GetSpan();
double mean = CalculateMean(values);
var (m3, m2) = CalculateMoments(values, mean);
skew = CalculateSkewness(m3, m2, values.Length);
}
IsHot = _buffer.Count >= Period;
+48 -25
View File
@@ -1,5 +1,4 @@
using System;
using System.Linq;
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
@@ -43,11 +42,14 @@ namespace QuanTAlib;
/// Note: Provides additional regression statistics (R², intercept)
/// </remarks>
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;
/// <summary>Gets the y-intercept of the regression line.</summary>
public double? Intercept { get; private set; }
@@ -63,6 +65,7 @@ public class Slope : AbstractBase
/// <param name="period">The number of points to consider for slope calculation.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than or equal to 1.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Slope(int period)
{
if (period <= 1)
@@ -80,12 +83,14 @@ public class Slope : AbstractBase
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The number of points to consider for slope calculation.</param>
[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<double> 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<double> 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<double> 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;
}
+37 -9
View File
@@ -1,5 +1,4 @@
using System;
using System.Linq;
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
@@ -44,17 +43,21 @@ namespace QuanTAlib;
/// Note: Foundation for many volatility-based indicators
/// </remarks>
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;
/// <param name="period">The number of points to consider for standard deviation calculation.</param>
/// <param name="isPopulation">True for population stddev, false for sample stddev (default).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than 2.</exception>
[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
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The number of points to consider for standard deviation calculation.</param>
/// <param name="isPopulation">True for population stddev, false for sample stddev (default).</param>
[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<double> 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<double> 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<double> 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;
+37 -9
View File
@@ -1,5 +1,4 @@
using System;
using System.Linq;
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
@@ -44,17 +43,21 @@ namespace QuanTAlib;
/// Note: Basis for Modern Portfolio Theory and risk models
/// </remarks>
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;
/// <param name="period">The number of points to consider for variance calculation.</param>
/// <param name="isPopulation">True for population variance, false for sample variance (default).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than 2.</exception>
[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
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The number of points to consider for variance calculation.</param>
/// <param name="isPopulation">True for population variance, false for sample variance (default).</param>
[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<double> 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<double> 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<double> 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;
+40 -14
View File
@@ -1,5 +1,4 @@
using System;
using System.Linq;
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
@@ -43,22 +42,26 @@ namespace QuanTAlib;
/// Note: Assumes approximately normal distribution
/// </remarks>
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;
/// <param name="period">The number of points to consider for Z-score calculation.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than 2.</exception>
[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
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The number of points to consider for Z-score calculation.</param>
[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<double> 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<double> 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<double> 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;
}