Refactor and optimize various components of QuanTAlib

- Removed WmaVector class to streamline weighted moving average calculations.
- Simplified RingBuffer implementation by removing unnecessary comments and improving clarity.
- Enhanced SIMD extensions for better performance and readability.
- Updated TBar and TBarSeries classes to improve property calculations and reduce overhead.
- Cleaned up TValue struct by removing redundant comments.
- Added comprehensive unit tests for IndicatorExtensions and TrimaIndicator to ensure functionality and correctness.
This commit is contained in:
Miha Kralj
2025-12-04 13:49:05 -08:00
parent 3ed35322a5
commit 967096d4f5
27 changed files with 387 additions and 3367 deletions
+3 -7
View File
@@ -23,9 +23,9 @@ public sealed class RingBuffer : IEnumerable<double>
{
private readonly double[] _buffer;
private readonly int _capacity;
private int _head; // Next write position (also start position when full)
private int _count; // Current number of elements
private double _sum; // Running sum of all elements
private int _head;
private int _count;
private double _sum;
/// <summary>
/// Creates a new RingBuffer with the specified capacity.
@@ -114,7 +114,6 @@ public sealed class RingBuffer : IEnumerable<double>
get
{
if (_count == 0) return 0;
// When full, _head points to oldest; otherwise start is 0
int start = _count == _capacity ? _head : 0;
return _buffer[start];
}
@@ -143,7 +142,6 @@ public sealed class RingBuffer : IEnumerable<double>
if (_count == _capacity)
{
// Buffer is full: remove oldest value from sum
removed = _buffer[_head];
_sum -= removed;
}
@@ -243,13 +241,11 @@ public sealed class RingBuffer : IEnumerable<double>
int start = _count == _capacity ? _head : 0;
// Check if contiguous (no wrap)
if (start + _count <= _capacity)
{
return new ReadOnlySpan<double>(_buffer, start, _count);
}
// Wrapped - need to copy
return new ReadOnlySpan<double>(ToArray());
}
+10 -21
View File
@@ -112,7 +112,6 @@ public static class SimdExtensions
return true;
}
// Check remaining elements with scalar
for (; i < span.Length; i++)
{
if (!double.IsFinite(span[i]))
@@ -144,19 +143,16 @@ public static class SimdExtensions
int vectorSize = Vector<double>.Count;
int i = 0;
// Process in vector chunks
for (; i <= span.Length - vectorSize; i += vectorSize)
{
var vector = new Vector<double>(span.Slice(i, vectorSize));
sum += vector;
}
// Horizontal sum of vector
double result = 0.0;
for (int j = 0; j < vectorSize; j++)
result += sum[j];
// Process remaining elements
for (; i < span.Length; i++)
result += span[i];
@@ -186,14 +182,12 @@ public static class SimdExtensions
var minVec = new Vector<double>(span.Slice(0, vectorSize));
int i = vectorSize;
// Process in vector chunks
for (; i <= span.Length - vectorSize; i += vectorSize)
{
var vector = new Vector<double>(span.Slice(i, vectorSize));
minVec = Vector.Min(minVec, vector);
}
// Find minimum within vector
double result = minVec[0];
for (int j = 1; j < vectorSize; j++)
{
@@ -201,7 +195,6 @@ public static class SimdExtensions
result = minVec[j];
}
// Process remaining elements
for (; i < span.Length; i++)
{
if (span[i] < result)
@@ -234,14 +227,12 @@ public static class SimdExtensions
var maxVec = new Vector<double>(span.Slice(0, vectorSize));
int i = vectorSize;
// Process in vector chunks
for (; i <= span.Length - vectorSize; i += vectorSize)
{
var vector = new Vector<double>(span.Slice(i, vectorSize));
maxVec = Vector.Max(maxVec, vector);
}
// Find maximum within vector
double result = maxVec[0];
for (int j = 1; j < vectorSize; j++)
{
@@ -249,7 +240,6 @@ public static class SimdExtensions
result = maxVec[j];
}
// Process remaining elements
for (; i < span.Length; i++)
{
if (span[i] > result)
@@ -285,12 +275,17 @@ public static class SimdExtensions
{
if (span.Length < 2) return double.NaN;
// Guard against non-finite inputs
if (span.ContainsNonFinite()) return double.NaN;
double m;
if (mean.HasValue)
{
if (span.ContainsNonFinite()) return double.NaN;
m = mean.Value;
}
else
{
m = span.AverageSIMD();
}
double m = mean ?? span.AverageSIMD();
// Guard against non-finite mean (could be passed in or computed from non-finite values)
if (!double.IsFinite(m)) return double.NaN;
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
@@ -300,7 +295,6 @@ public static class SimdExtensions
int vectorSize = Vector<double>.Count;
int i = 0;
// Process in vector chunks
for (; i <= span.Length - vectorSize; i += vectorSize)
{
var vector = new Vector<double>(span.Slice(i, vectorSize));
@@ -308,12 +302,10 @@ public static class SimdExtensions
sumSq += diff * diff;
}
// Horizontal sum of vector
double result = 0.0;
for (int j = 0; j < vectorSize; j++)
result += sumSq[j];
// Process remaining elements
for (; i < span.Length; i++)
{
double diff = span[i] - m;
@@ -358,7 +350,6 @@ public static class SimdExtensions
var maxVec = minVec;
int i = vectorSize;
// Process in vector chunks
for (; i <= span.Length - vectorSize; i += vectorSize)
{
var vector = new Vector<double>(span.Slice(i, vectorSize));
@@ -366,7 +357,6 @@ public static class SimdExtensions
maxVec = Vector.Max(maxVec, vector);
}
// Find min/max within vectors
double min = minVec[0];
double max = maxVec[0];
for (int j = 1; j < vectorSize; j++)
@@ -375,7 +365,6 @@ public static class SimdExtensions
if (maxVec[j] > max) max = maxVec[j];
}
// Process remaining elements
for (; i < span.Length; i++)
{
if (span[i] < min) min = span[i];
+2 -2
View File
@@ -28,8 +28,8 @@ public readonly struct TBar : IEquatable<TBar>
// Computed properties (calculated on demand, no storage overhead)
public double HL2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low) * 0.5; }
public double OC2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + Close) * 0.5; }
public double OHL3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + High + Low) / 3.0; }
public double HLC3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close) / 3.0; }
public double OHL3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + High + Low) * 0.333333333333333333; }
public double HLC3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close) * 0.333333333333333333; }
public double OHLC4 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + High + Low + Close) * 0.25; }
public double HLCC4 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close + Close) * 0.25; }
-7
View File
@@ -11,7 +11,6 @@ namespace QuanTAlib;
/// </summary>
public class TBarSeries : IReadOnlyList<TBar>
{
// Internal storage: SoA layout
protected readonly List<long> _t = new();
protected readonly List<double> _o = new();
protected readonly List<double> _h = new();
@@ -22,7 +21,6 @@ public class TBarSeries : IReadOnlyList<TBar>
public string Name { get; set; } = "Bar";
public event Action<TBar>? Pub;
// Public properties are Views into the main data
public TSeries Open { get; }
public TSeries High { get; }
public TSeries Low { get; }
@@ -38,7 +36,6 @@ public class TBarSeries : IReadOnlyList<TBar>
public TBarSeries()
{
// Initialize views sharing the same Time list but different Value lists
Open = new TSeries(_t, _o) { Name = "Open" };
High = new TSeries(_t, _h) { Name = "High" };
Low = new TSeries(_t, _l) { Name = "Low" };
@@ -46,9 +43,6 @@ public class TBarSeries : IReadOnlyList<TBar>
Volume = new TSeries(_t, _v) { Name = "Volume" };
}
/// <summary>
/// Constructor with capacity hint to avoid List growth overhead.
/// </summary>
public TBarSeries(int capacity)
{
_t = new List<long>(capacity);
@@ -58,7 +52,6 @@ public class TBarSeries : IReadOnlyList<TBar>
_c = new List<double>(capacity);
_v = new List<double>(capacity);
// Initialize views sharing the same Time list but different Value lists
Open = new TSeries(_t, _o) { Name = "Open" };
High = new TSeries(_t, _h) { Name = "High" };
Low = new TSeries(_t, _l) { Name = "Low" };
+3 -15
View File
@@ -11,36 +11,25 @@ namespace QuanTAlib;
/// </summary>
public class TSeries : IReadOnlyList<TValue>
{
// Internal storage: SoA layout
// We use List<T> for dynamic sizing but access internal arrays via CollectionsMarshal for speed
protected readonly List<long> _t;
protected readonly List<double> _v;
public string Name { get; set; } = "Data";
// Event optimization: Use Action<TValue> to avoid EventArgs allocation
// Note: Events are generally discouraged in the hot path of this high-perf design,
// but kept for compatibility/chaining.
public event Action<TValue>? Pub;
public TSeries()
public TSeries()
{
_t = new List<long>();
_v = new List<double>();
}
/// <summary>
/// Constructor with capacity hint to avoid List growth overhead.
/// </summary>
public TSeries(int capacity)
public TSeries(int capacity)
{
_t = new List<long>(capacity);
_v = new List<double>(capacity);
}
/// <summary>
/// Constructor for wrapping existing lists (e.g. from TBarSeries).
/// </summary>
public TSeries(List<long> time, List<double> values)
{
_t = time;
@@ -105,7 +94,6 @@ public class TSeries : IReadOnlyList<TValue>
}
else
{
// Update last bar
int lastIdx = _v.Count - 1;
_t[lastIdx] = value.Time;
_v[lastIdx] = value.Value;
@@ -129,7 +117,7 @@ public class TSeries : IReadOnlyList<TValue>
foreach (var v in values)
{
Add(new TValue(t, v), isNew: true);
t += TimeSpan.TicksPerMinute; // Dummy time increment
t += TimeSpan.TicksPerMinute;
}
}
-10
View File
@@ -9,19 +9,9 @@ namespace QuanTAlib;
[SkipLocalsInit]
public readonly struct TValue : IEquatable<TValue>
{
/// <summary>
/// Time in ticks (UTC).
/// </summary>
public readonly long Time;
/// <summary>
/// The value.
/// </summary>
public readonly double Value;
/// <summary>
/// Convenience property to get DateTime from Ticks.
/// </summary>
public DateTime AsDateTime => new(Time, DateTimeKind.Utc);
[MethodImpl(MethodImplOptions.AggressiveInlining)]