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
+5 -59
View File
@@ -21,7 +21,7 @@ This notebook demonstrates:
1. **Manual Data Processing**: Understanding Batch vs. Streaming modes.
2. **Streaming with `isNew`**: Handling intra-bar updates.
3. **Large Dataset Processing**: Using Geometric Brownian Motion (GBM) generated data.
4. **Vectorized Operations**: Calculating multiple WMAs simultaneously.
4. **Handling Invalid Values**: Last-value substitution for NaN/Infinity.
5. **WMA vs SMA vs EMA**: Comparing different moving averages.
#!csharp
@@ -190,63 +190,9 @@ Console.WriteLine($"Match: {Math.Abs(batchLargeResult.Last().Value - lastStreamV
#!markdown
## 4. Vectorized WMA (Multiple Periods)
## 4. Handling Invalid Values (NaN/Infinity)
`WmaVector` allows calculating multiple WMAs (e.g., 5, 10, 20) simultaneously. This is useful for comparing different timeframes.
### Vectorized Batch
#!csharp
int[] periods = { 5, 10, 20 };
Console.WriteLine($"\n--- Vectorized Batch WMA (Periods: {string.Join(", ", periods)}) ---");
var wmaVectorBatch = new WmaVector(periods);
var vectorBatchResults = wmaVectorBatch.Calculate(closeSeries);
for (int i = 0; i < periods.Length; i++)
{
Console.WriteLine($"WMA({periods[i]}) Last Value: {vectorBatchResults[i].Last().Value:F4}");
}
#!markdown
### Vectorized Streaming
#!csharp
Console.WriteLine($"\n--- Vectorized Streaming WMA (Periods: {string.Join(", ", periods)}) ---");
var wmaVectorStream = new WmaVector(periods);
TValue[] lastVectorVal = null;
foreach(var item in closeSeries)
{
lastVectorVal = wmaVectorStream.Update(item);
}
for (int i = 0; i < periods.Length; i++)
{
Console.WriteLine($"WMA({periods[i]}) Last Value: {lastVectorVal[i].Value:F4}");
}
// Verification
bool allMatch = true;
for (int i = 0; i < periods.Length; i++)
{
if (Math.Abs(vectorBatchResults[i].Last().Value - lastVectorVal[i].Value) > 1e-10)
{
allMatch = false;
break;
}
}
Console.WriteLine($"\nAll Vectorized Stream/Batch values match: {allMatch}");
#!markdown
## 5. Handling Invalid Values (NaN/Infinity)
Both `Wma` and `WmaVector` use **last-value substitution** for invalid inputs. When a non-finite value (NaN, PositiveInfinity, NegativeInfinity) is encountered, it is replaced with the last valid value. This provides output continuity instead of propagating invalid values through the calculation.
`Wma` uses **last-value substitution** for invalid inputs. When a non-finite value (NaN, PositiveInfinity, NegativeInfinity) is encountered, it is replaced with the last valid value. This provides output continuity instead of propagating invalid values through the calculation.
#!csharp
@@ -299,7 +245,7 @@ for (int i = 0; i < seriesWithNaN.Count; i++)
#!markdown
## 6. WMA vs SMA vs EMA Comparison
## 5. WMA vs SMA vs EMA Comparison
The WMA, SMA, and EMA are all trend-following indicators, but they weight data differently:
@@ -346,7 +292,7 @@ Console.WriteLine("- WMA provides a balance between SMA's stability and EMA's re
#!markdown
## 7. WMA Weights More Recent Values
## 6. WMA Weights More Recent Values
This example demonstrates how WMA weights more recent values compared to SMA.
+24 -211
View File
@@ -13,29 +13,12 @@ namespace QuanTAlib;
/// WMA applies linear weighting to data points, giving more weight to recent values.
/// Uses dual running sums for O(1) complexity per update.
///
/// Key characteristics:
/// - Linear weighting: newest value has weight n, oldest has weight 1
/// - More responsive than SMA due to emphasis on recent data
/// - Less lag than SMA, but more than EMA
/// - O(1) time complexity for both update and bar correction
/// - O(1) space complexity for state save/restore (scalars only)
/// Calculation:
/// WMA = (n*P_n + (n-1)*P_(n-1) + ... + 1*P_1) / (n*(n+1)/2)
///
/// Calculation method:
/// WMA = (n*P_n + (n-1)*P_(n-1) + ... + 2*P_2 + 1*P_1) / (n*(n+1)/2)
///
/// O(1) update formula:
/// O(1) update:
/// S_new = S - oldest + newest
/// W_new = W - S_old + n*newest
/// WMA = W_new / divisor
///
/// Bar correction (isNew=false):
/// - Restores to state after last isNew=true
/// - Then replaces the last value with new correction value
/// - All O(1) using scalar state
///
/// Sources:
/// - https://www.investopedia.com/terms/w/weightedaverage.asp
/// - https://school.stockcharts.com/doku.php?id=technical_indicators:weighted_moving_average
/// </remarks>
[SkipLocalsInit]
public sealed class Wma
@@ -44,32 +27,19 @@ public sealed class Wma
private readonly double _divisor;
private readonly RingBuffer _buffer;
// Dual running sums for O(1) WMA calculation
private double _sum; // Simple sum of values in window
private double _wsum; // Weighted sum of values in window
private double _p_sum; // Sum AFTER last isNew=true (for correction restore)
private double _p_wsum; // Weighted sum AFTER last isNew=true
private double _p_lastInput; // Input that was added on last isNew=true
private double _lastValidValue;
private double _p_lastValidValue;
private int _tickCount; // Counter for periodic sum resync
// Resync interval: recalculate sum from buffer every N ticks to prevent drift
private double _sum, _wsum;
private double _p_sum, _p_wsum, _p_lastInput;
private double _lastValidValue, _p_lastValidValue;
private int _tickCount;
private const int ResyncInterval = 1000;
/// <summary>
/// Display name for the indicator.
/// </summary>
public string Name { get; }
public TValue Value { get; private set; }
public bool IsHot => _buffer.IsFull;
/// <summary>
/// Creates WMA with specified period.
/// </summary>
/// <param name="period">Number of values to average (must be > 0)</param>
public Wma(int period)
{
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period));
_period = period;
_divisor = period * (period + 1) * 0.5;
@@ -77,20 +47,6 @@ public sealed class Wma
Name = $"Wma({period})";
}
/// <summary>
/// Current WMA value.
/// </summary>
public TValue Value { get; private set; }
/// <summary>
/// True if the WMA has enough data to produce valid results.
/// WMA is "hot" when the buffer is full (has received at least 'period' values).
/// </summary>
public bool IsHot => _buffer.IsFull;
/// <summary>
/// Gets a valid input value, using last-value substitution for non-finite inputs.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidValue(double input)
{
@@ -102,33 +58,25 @@ public sealed class Wma
return _lastValidValue;
}
/// <summary>
/// Updates internal state with a new value.
/// Shared logic for both streaming and batch-reconstruction.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UpdateState(double val)
{
if (_buffer.IsFull)
{
// Buffer is full: O(1) update using dual running sums
double oldSum = _sum; // Capture before update
double oldSum = _sum;
double oldest = _buffer.Oldest;
_sum = _sum - oldest + val;
_wsum = _wsum - oldSum + (_period * val);
}
else
{
// Warmup phase: incrementally build sums
int count = _buffer.Count + 1;
_sum += val;
_wsum += count * val;
}
// Update buffer
_buffer.Add(val);
// Periodic resync: recalculate sums from scratch to eliminate floating-point drift
_tickCount++;
if (_buffer.IsFull && _tickCount >= ResyncInterval)
{
@@ -147,24 +95,14 @@ public sealed class Wma
}
}
/// <summary>
/// Updates WMA with the given value.
/// O(1) for both isNew=true and isNew=false.
/// </summary>
/// <param name="input">Input value</param>
/// <param name="isNew">True for new bar, false for update to current bar (default: true)</param>
/// <returns>Current WMA value</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
// Get valid value (this may update _lastValidValue)
double val = GetValidValue(input.Value);
UpdateState(val);
// Save state AFTER this update for potential future corrections
_p_sum = _sum;
_p_wsum = _wsum;
_p_lastInput = val;
@@ -172,40 +110,24 @@ public sealed class Wma
}
else
{
// Bar correction: restore to state AFTER last isNew=true, then swap last value
// Restore _lastValidValue BEFORE calling GetValidValue
_lastValidValue = _p_lastValidValue;
// Get valid value (this may update _lastValidValue)
double val = GetValidValue(input.Value);
// Restore sums to state after last isNew=true
_sum = _p_sum;
_wsum = _p_wsum;
// Correction: replace _p_lastInput with val
// S_corrected = S - lastInput + val
// W_corrected = W + weight*(val - lastInput), where weight = period (if full) or count (if warmup)
int weight = _buffer.IsFull ? _period : _buffer.Count;
_sum = _sum - _p_lastInput + val;
_wsum += weight * (val - _p_lastInput);
// Update buffer's newest value
_buffer.UpdateNewest(val);
}
// Calculate WMA using current divisor (handles warmup)
double currentDivisor = _buffer.IsFull ? _divisor : _buffer.Count * (_buffer.Count + 1) * 0.5;
double result = _wsum / currentDivisor;
Value = new TValue(input.Time, result);
Value = new TValue(input.Time, _wsum / currentDivisor);
return Value;
}
/// <summary>
/// Updates WMA with the entire series.
/// </summary>
/// <param name="source">Input series</param>
/// <returns>WMA series</returns>
public TSeries Update(TSeries source)
{
if (source.Count == 0) return new TSeries(new List<long>(), new List<double>());
@@ -218,41 +140,30 @@ public sealed class Wma
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
var sourceValues = source.Values;
var sourceTimes = source.Times;
// 1. Fast Batch Calculation (SIMD optimized)
Calculate(sourceValues, vSpan, _period);
// 2. Copy Times
sourceTimes.CopyTo(tSpan);
// 3. Reconstruct State for subsequent updates
// We need to restore _buffer, _sum, _wsum, and _lastValidValue
// Find the last valid value before the reconstruction window
Calculate(source.Values, vSpan, _period);
source.Times.CopyTo(tSpan);
// Restore state
int windowSize = Math.Min(len, _period);
int startIndex = len - windowSize;
// Restore _lastValidValue from before the window
if (startIndex > 0)
{
// Scan backwards to find last valid value
for (int i = startIndex - 1; i >= 0; i--)
{
if (double.IsFinite(sourceValues[i]))
if (double.IsFinite(source.Values[i]))
{
_lastValidValue = sourceValues[i];
_lastValidValue = source.Values[i];
break;
}
}
}
else
{
_lastValidValue = 0; // Reset if starting from 0
_lastValidValue = 0;
}
// Rebuild buffer and sums from last 'period' values using shared logic
_buffer.Clear();
_sum = 0;
_wsum = 0;
@@ -260,41 +171,25 @@ public sealed class Wma
for (int i = startIndex; i < len; i++)
{
double val = GetValidValue(sourceValues[i]);
double val = GetValidValue(source.Values[i]);
UpdateState(val);
}
// Save state for potential future corrections
_p_sum = _sum;
_p_wsum = _wsum;
_p_lastInput = sourceValues[len - 1];
_p_lastInput = source.Values[len - 1];
_p_lastValidValue = _lastValidValue;
Value = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v);
}
/// <summary>
/// Calculates WMA for the entire series using a new instance.
/// </summary>
/// <param name="source">Input series</param>
/// <param name="period">WMA period</param>
/// <returns>WMA series</returns>
public static TSeries Calculate(TSeries source, int period)
{
var wma = new Wma(period);
return wma.Update(source);
}
/// <summary>
/// Calculates WMA in-place, writing results to pre-allocated output span.
/// Zero-allocation method for maximum performance.
/// Uses O(1) dual running sum algorithm.
/// Automatically uses SIMD acceleration for large, clean datasets.
/// </summary>
/// <param name="source">Input values</param>
/// <param name="output">Output span (must be same length as source)</param>
/// <param name="period">WMA period (must be > 0)</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
{
@@ -306,8 +201,6 @@ public sealed class Wma
int len = source.Length;
if (len == 0) return;
// Try SIMD path for large, clean datasets
// Requirements: AVX2 support, large enough dataset, no NaN values
const int SimdThreshold = 256;
if (Avx2.IsSupported && len >= SimdThreshold && !HasNonFiniteValues(source))
{
@@ -318,11 +211,6 @@ public sealed class Wma
CalculateScalarCore(source, output, period);
}
/// <summary>
/// Scalar implementation with NaN handling via last-value substitution.
/// Uses circular buffer for sliding window calculation.
/// Optimized with split loops and periodic resync.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CalculateScalarCore(ReadOnlySpan<double> source, Span<double> output, int period)
{
@@ -332,12 +220,10 @@ public sealed class Wma
double wsum = 0;
double lastValid = 0;
// Ring buffer simulation
Span<double> buffer = period <= 512 ? stackalloc double[period] : new double[period];
int bufferIdx = 0;
int i = 0;
// Phase 1: Warmup (0 to period-1)
int warmupEnd = Math.Min(period, len);
for (; i < warmupEnd; i++)
{
@@ -355,7 +241,6 @@ public sealed class Wma
output[i] = wsum / currentDivisor;
}
// Phase 2: Hot loop (period to len)
int tickCount = 0;
for (; i < len; i++)
{
@@ -365,7 +250,6 @@ public sealed class Wma
else
val = lastValid;
// O(1) update using dual running sums
double oldSum = sum;
double oldest = buffer[bufferIdx];
sum = sum - oldest + val;
@@ -378,28 +262,17 @@ public sealed class Wma
output[i] = wsum / divisor;
// Periodic resync every 1000 ticks
tickCount++;
if (tickCount >= ResyncInterval)
{
tickCount = 0;
// Recalculate sums from buffer to prevent drift
double recalcSum = 0;
double recalcWsum = 0;
// Buffer contains values in order: [oldest ... newest] relative to current bufferIdx
// Actually buffer is circular.
// Oldest is at bufferIdx (which we just wrote to, so it's actually newest now? No, we incremented bufferIdx)
// bufferIdx points to the *next* overwrite location, which holds the *oldest* value.
// So buffer[bufferIdx] is oldest (weight 1).
// buffer[bufferIdx+1] is 2nd oldest (weight 2).
// ...
// buffer[bufferIdx-1] is newest (weight period).
for (int k = 0; k < period; k++)
{
int idx = (bufferIdx + k) % period; // Use modulo here for simplicity in resync (rare)
// Wait, modulo is slow.
if (idx >= period) idx -= period; // Manual modulo
int idx = bufferIdx + k;
if (idx >= period) idx -= period;
double v = buffer[idx];
recalcSum += v;
@@ -411,10 +284,6 @@ public sealed class Wma
}
}
/// <summary>
/// SIMD-optimized implementation for WMA calculation.
/// Uses double prefix-sum approach to vectorize the coupled recurrence.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private static unsafe void CalculateSimdCore(ReadOnlySpan<double> source, Span<double> output, int period)
{
@@ -427,7 +296,6 @@ public sealed class Wma
double divisor = period * (period + 1) * 0.5;
double invDivisor = 1.0 / divisor;
// Phase 1: Warmup - scalar
int warmupEnd = Math.Min(period, len);
double sum = 0;
double wsum = 0;
@@ -443,13 +311,11 @@ public sealed class Wma
if (len <= period)
return;
// Phase 2: SIMD hot loop
var vInvDivisor = Vector256.Create(invDivisor);
var vPeriod = Vector256.Create((double)period);
var vZero = Vector256<double>.Zero;
int simdEnd = period + ((len - period) / VectorWidth) * VectorWidth;
// Initialize vector state
var vSumState = Vector256.Create(sum);
var vWsumState = Vector256.Create(wsum);
@@ -458,26 +324,17 @@ public sealed class Wma
{
int nextSync = Math.Min(simdEnd, idx + ResyncInterval);
// Inner hot loop without branches
// Unrolled 2x (process 8 doubles per iteration)
// Optimized Parallel Execution:
// - Parallel prefix sums for DeltaS
// - Fast S_shifted calculation using (S - DeltaS)
// - Parallel prefix sums for U
int unrolledSync = nextSync - (2 * VectorWidth);
for (; idx <= unrolledSync; idx += 2 * VectorWidth)
{
// Load data for both iterations
var vNew1 = Avx.LoadVector256(srcPtr + idx);
var vOld1 = Avx.LoadVector256(srcPtr + idx - period);
var vNew2 = Avx.LoadVector256(srcPtr + idx + VectorWidth);
var vOld2 = Avx.LoadVector256(srcPtr + idx + VectorWidth - period);
// 1. Update Sum (S) - Parallel Prefix Sums
var vDeltaS1 = Avx.Subtract(vNew1, vOld1);
var vDeltaS2 = Avx.Subtract(vNew2, vOld2);
// Prefix Sum DeltaS1
var vShiftS1_1 = Avx2.Permute4x64(vDeltaS1.AsUInt64(), 0b_10_01_00_00).AsDouble();
vShiftS1_1 = Avx.Blend(vZero, vShiftS1_1, 0b_1110);
var vPS_DeltaS1 = Avx.Add(vDeltaS1, vShiftS1_1);
@@ -485,7 +342,6 @@ public sealed class Wma
vShiftS2_1 = Avx.Blend(vZero, vShiftS2_1, 0b_1100);
vPS_DeltaS1 = Avx.Add(vPS_DeltaS1, vShiftS2_1);
// Prefix Sum DeltaS2
var vShiftS1_2 = Avx2.Permute4x64(vDeltaS2.AsUInt64(), 0b_10_01_00_00).AsDouble();
vShiftS1_2 = Avx.Blend(vZero, vShiftS1_2, 0b_1110);
var vPS_DeltaS2 = Avx.Add(vDeltaS2, vShiftS1_2);
@@ -493,14 +349,10 @@ public sealed class Wma
vShiftS2_2 = Avx.Blend(vZero, vShiftS2_2, 0b_1100);
vPS_DeltaS2 = Avx.Add(vPS_DeltaS2, vShiftS2_2);
// Combine Sums
var vSums1 = Avx.Add(vSumState, vPS_DeltaS1);
var vLastS1 = Avx2.Permute4x64(vSums1.AsUInt64(), 0b_11_11_11_11).AsDouble();
var vSums2 = Avx.Add(vLastS1, vPS_DeltaS2);
// 2. Update Weighted Sum (W)
// Optimization: S_shifted = S - DeltaS
// This avoids expensive Permute/Blend operations
var vSumsShifted1 = Avx.Subtract(vSums1, vDeltaS1);
var vSumsShifted2 = Avx.Subtract(vSums2, vDeltaS2);
@@ -516,7 +368,6 @@ public sealed class Wma
vU2 = Avx.Subtract(Avx.Multiply(vPeriod, vNew2), vSumsShifted2);
}
// Prefix Sum W1
var vShiftW1_1 = Avx2.Permute4x64(vU1.AsUInt64(), 0b_10_01_00_00).AsDouble();
vShiftW1_1 = Avx.Blend(vZero, vShiftW1_1, 0b_1110);
var vPW1_1 = Avx.Add(vU1, vShiftW1_1);
@@ -524,7 +375,6 @@ public sealed class Wma
vShiftW2_1 = Avx.Blend(vZero, vShiftW2_1, 0b_1100);
var vPW2_1 = Avx.Add(vPW1_1, vShiftW2_1);
// Prefix Sum W2
var vShiftW1_2 = Avx2.Permute4x64(vU2.AsUInt64(), 0b_10_01_00_00).AsDouble();
vShiftW1_2 = Avx.Blend(vZero, vShiftW1_2, 0b_1110);
var vPW1_2 = Avx.Add(vU2, vShiftW1_2);
@@ -532,32 +382,24 @@ public sealed class Wma
vShiftW2_2 = Avx.Blend(vZero, vShiftW2_2, 0b_1100);
var vPW2_2 = Avx.Add(vPW1_2, vShiftW2_2);
// Combine Weighted Sums
var vWsums1 = Avx.Add(vWsumState, vPW2_1);
var vLastW1 = Avx2.Permute4x64(vWsums1.AsUInt64(), 0b_11_11_11_11).AsDouble();
var vWsums2 = Avx.Add(vLastW1, vPW2_2);
// Store results
Avx.Store(outPtr + idx, Avx.Multiply(vWsums1, vInvDivisor));
Avx.Store(outPtr + idx + VectorWidth, Avx.Multiply(vWsums2, vInvDivisor));
// Update state for next iteration
vSumState = Avx2.Permute4x64(vSums2.AsUInt64(), 0b_11_11_11_11).AsDouble();
vWsumState = Avx2.Permute4x64(vWsums2.AsUInt64(), 0b_11_11_11_11).AsDouble();
}
// Handle remaining vectors (if any)
for (; idx < nextSync; idx += VectorWidth)
{
// Load 4 entering values and 4 leaving values
var vNew = Avx.LoadVector256(srcPtr + idx);
var vOld = Avx.LoadVector256(srcPtr + idx - period);
// 1. Update Sum (S)
// Delta S = New - Old
var vDeltaS = Avx.Subtract(vNew, vOld);
// Prefix sum of Delta S
var vShiftS1 = Avx2.Permute4x64(vDeltaS.AsUInt64(), 0b_10_01_00_00).AsDouble();
vShiftS1 = Avx.Blend(vZero, vShiftS1, 0b_1110);
var vPS1 = Avx.Add(vDeltaS, vShiftS1);
@@ -566,19 +408,14 @@ public sealed class Wma
vShiftS2 = Avx.Blend(vZero, vShiftS2, 0b_1100);
var vPS2 = Avx.Add(vPS1, vShiftS2);
// Add previous sum state
var vSums = Avx.Add(vSumState, vPS2);
// 2. Update Weighted Sum (W)
// Shift vSums right and insert sum (S_t) at pos 0
var vSumsShifted = Avx2.Permute4x64(vSums.AsUInt64(), 0b_10_01_00_00).AsDouble();
vSumsShifted = Avx.Blend(vSumState, vSumsShifted, 0b_1110);
// U = (n * New) - S_shifted
var vTerm1 = Avx.Multiply(vPeriod, vNew);
var vU = Avx.Subtract(vTerm1, vSumsShifted);
// Prefix sum of U
var vShiftW1 = Avx2.Permute4x64(vU.AsUInt64(), 0b_10_01_00_00).AsDouble();
vShiftW1 = Avx.Blend(vZero, vShiftW1, 0b_1110);
var vPW1 = Avx.Add(vU, vShiftW1);
@@ -587,26 +424,17 @@ public sealed class Wma
vShiftW2 = Avx.Blend(vZero, vShiftW2, 0b_1100);
var vPW2 = Avx.Add(vPW1, vShiftW2);
// Add previous wsum state
var vWsums = Avx.Add(vWsumState, vPW2);
// Store result
var vResult = Avx.Multiply(vWsums, vInvDivisor);
Avx.Store(outPtr + idx, vResult);
// Update state for next iteration
vSumState = Avx2.Permute4x64(vSums.AsUInt64(), 0b_11_11_11_11).AsDouble();
vWsumState = Avx2.Permute4x64(vWsums.AsUInt64(), 0b_11_11_11_11).AsDouble();
}
// Periodic resync
if (idx < len)
{
// Extract scalar state for resync logic
sum = vSumState.GetElement(0);
wsum = vWsumState.GetElement(0);
// Recalculate sums from scratch
int lastIdx = idx - 1;
double recalcSum = 0;
double recalcWsum = 0;
@@ -619,17 +447,14 @@ public sealed class Wma
sum = recalcSum;
wsum = recalcWsum;
// Update vector state after resync
vSumState = Vector256.Create(sum);
vWsumState = Vector256.Create(wsum);
}
}
// Extract final scalar state for tail
sum = vSumState.GetElement(0);
wsum = vWsumState.GetElement(0);
// Phase 3: Scalar tail
for (; idx < len; idx++)
{
double val = srcPtr[idx];
@@ -642,9 +467,6 @@ public sealed class Wma
}
}
/// <summary>
/// Checks if span contains any non-finite values (NaN or Infinity).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool HasNonFiniteValues(ReadOnlySpan<double> span)
{
@@ -656,19 +478,10 @@ public sealed class Wma
return false;
}
/// <summary>
/// Resets the WMA state.
/// </summary>
public void Reset()
{
_buffer.Clear();
_sum = 0;
_wsum = 0;
_p_sum = 0;
_p_wsum = 0;
_p_lastInput = 0;
_lastValidValue = 0;
_p_lastValidValue = 0;
_sum = _wsum = _p_sum = _p_wsum = _p_lastInput = _lastValidValue = _p_lastValidValue = 0;
Value = default;
}
}
+2 -26
View File
@@ -114,33 +114,9 @@ Console.WriteLine($"Last WMA: {wmaOutput[^1]}");
* **O(1) per-bar** via dual running sums
* **Compatible** with `ArrayPool<T>` for buffer management
### Multi-Period WMA (`WmaVector`)
The `WmaVector` class calculates multiple WMAs with different periods on the same input series simultaneously.
```csharp
using QuanTAlib;
// Initialize with multiple periods
int[] periods = { 5, 10, 20 };
var wmaVector = new WmaVector(periods);
// Streaming update
TValue[] results = wmaVector.Update(new TValue(time, price));
// Access values
Console.WriteLine($"WMA(5): {results[0].Value}");
Console.WriteLine($"WMA(10): {results[1].Value}");
Console.WriteLine($"WMA(20): {results[2].Value}");
// Batch calculation
TSeries source = ...;
TSeries[] seriesResults = wmaVector.Calculate(source);
```
### Bar Correction (isNew Parameter)
Both `Wma` and `WmaVector` support intra-bar updates for real-time trading systems:
`Wma` supports intra-bar updates for real-time trading systems:
```csharp
var wma = new Wma(10);
@@ -166,7 +142,7 @@ wma.Update(new TValue(time + 1, 101.2), isNew: true);
### Handling Invalid Values (NaN/Infinity)
Both `Wma` and `WmaVector` use **last-value substitution** for handling invalid inputs:
`Wma` uses **last-value substitution** for handling invalid inputs:
```csharp
var wma = new Wma(10);
-407
View File
@@ -1,407 +0,0 @@
namespace QuanTAlib.Tests;
public class WmaVectorTests
{
[Fact]
public void Initialization_WithPeriods_Works()
{
int[] periods = { 5, 10, 20 };
var wmaVector = new WmaVector(periods);
var res = wmaVector.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.Equal(3, res.Length);
Assert.Equal(100.0, res[0].Value, 1e-9);
Assert.Equal(100.0, res[1].Value, 1e-9);
Assert.Equal(100.0, res[2].Value, 1e-9);
}
[Fact]
public void Initialization_WithZeroPeriod_ThrowsArgumentException()
{
int[] periods = { 10, 0, 20 };
Assert.Throws<ArgumentOutOfRangeException>(() => new WmaVector(periods));
}
[Fact]
public void Initialization_WithNegativePeriod_ThrowsArgumentException()
{
int[] periods = { 10, -5, 20 };
Assert.Throws<ArgumentOutOfRangeException>(() => new WmaVector(periods));
}
[Fact]
public void Calc_Streaming_MatchesSingleWma()
{
int[] periods = { 5, 10, 20 };
var wmaVector = new WmaVector(periods);
var wmaSingles = periods.Select(p => new Wma(p)).ToArray();
var values = new double[] { 10, 20, 30, 40, 50, 40, 30, 20, 10 };
var time = DateTime.UtcNow;
foreach (var val in values)
{
var tVal = new TValue(time, val);
var multiRes = wmaVector.Update(tVal);
for (int i = 0; i < periods.Length; i++)
{
var singleRes = wmaSingles[i].Update(tVal);
Assert.Equal(singleRes.Value, multiRes[i].Value, 1e-9);
Assert.Equal(singleRes.Time, multiRes[i].Time);
}
time = time.AddMinutes(1);
}
}
[Fact]
public void Calc_Series_MatchesSingleWma()
{
int[] periods = { 5, 10, 20 };
var wmaVector = new WmaVector(periods);
int len = 100;
var t = new System.Collections.Generic.List<long>(len);
var v = new System.Collections.Generic.List<double>(len);
var now = DateTime.UtcNow;
for (int i = 0; i < len; i++)
{
t.Add(now.AddMinutes(i).Ticks);
v.Add(Math.Sin(i * 0.1) * 100);
}
var series = new TSeries(t, v);
var multiRes = wmaVector.Calculate(series);
// Reset and recalculate for comparison
var wmaSingles = periods.Select(p => new Wma(p)).ToArray();
for (int j = 0; j < len; j++)
{
var tVal = new TValue(new DateTime(t[j], DateTimeKind.Utc), v[j]);
for (int i = 0; i < periods.Length; i++)
{
var singleRes = wmaSingles[i].Update(tVal);
Assert.Equal(singleRes.Value, multiRes[i].Values[j], 1e-8);
}
}
}
[Fact]
public void Calc_Series_MatchesStreaming()
{
int[] periods = { 5, 10, 20 };
var wmaVectorBatch = new WmaVector(periods);
var wmaVectorStream = new WmaVector(periods);
int len = 100;
var t = new System.Collections.Generic.List<long>(len);
var v = new System.Collections.Generic.List<double>(len);
var now = DateTime.UtcNow;
for (int i = 0; i < len; i++)
{
t.Add(now.AddMinutes(i).Ticks);
v.Add(Math.Sin(i * 0.1) * 100);
}
var series = new TSeries(t, v);
var batchRes = wmaVectorBatch.Calculate(series);
for (int i = 0; i < len; i++)
{
var tVal = new TValue(new DateTime(t[i], DateTimeKind.Utc), v[i]);
var streamRes = wmaVectorStream.Update(tVal);
for (int j = 0; j < periods.Length; j++)
{
Assert.Equal(batchRes[j].Values[i], streamRes[j].Value, 1e-9);
}
}
}
[Fact]
public void Calculate_Static_MatchesInstanceMethod()
{
int[] periods = { 5, 10, 20 };
int len = 50;
var t = new System.Collections.Generic.List<long>(len);
var v = new System.Collections.Generic.List<double>(len);
var now = DateTime.UtcNow;
for (int i = 0; i < len; i++)
{
t.Add(now.AddMinutes(i).Ticks);
v.Add(Math.Sin(i * 0.1) * 100);
}
var series = new TSeries(t, v);
var instanceWma = new WmaVector(periods);
var instanceRes = instanceWma.Calculate(series);
var staticRes = WmaVector.Calculate(series, periods);
for (int i = 0; i < periods.Length; i++)
{
Assert.Equal(instanceRes[i].Count, staticRes[i].Count);
for (int j = 0; j < len; j++)
{
Assert.Equal(instanceRes[i].Values[j], staticRes[i].Values[j], 1e-9);
}
}
}
[Fact]
public void Reset_ClearsState()
{
int[] periods = { 10 };
var wmaVector = new WmaVector(periods);
wmaVector.Update(new TValue(DateTime.UtcNow, 100.0));
wmaVector.Update(new TValue(DateTime.UtcNow, 200.0));
wmaVector.Reset();
var res = wmaVector.Update(new TValue(DateTime.UtcNow, 50.0));
Assert.Equal(50.0, res[0].Value, 1e-9);
}
[Fact]
public void Update_NaN_Input_UsesLastValidValue()
{
int[] periods = { 10, 20 };
var wmaVector = new WmaVector(periods);
wmaVector.Update(new TValue(DateTime.UtcNow, 100.0));
wmaVector.Update(new TValue(DateTime.UtcNow, 110.0));
var resultAfterNaN = wmaVector.Update(new TValue(DateTime.UtcNow, double.NaN));
foreach (var result in resultAfterNaN)
{
Assert.True(double.IsFinite(result.Value), $"Expected finite value but got {result.Value}");
}
}
[Fact]
public void Update_Infinity_Input_UsesLastValidValue()
{
int[] periods = { 10, 20 };
var wmaVector = new WmaVector(periods);
wmaVector.Update(new TValue(DateTime.UtcNow, 100.0));
wmaVector.Update(new TValue(DateTime.UtcNow, 110.0));
var resultAfterPosInf = wmaVector.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
foreach (var result in resultAfterPosInf)
{
Assert.True(double.IsFinite(result.Value));
}
var resultAfterNegInf = wmaVector.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
foreach (var result in resultAfterNegInf)
{
Assert.True(double.IsFinite(result.Value));
}
}
[Fact]
public void Update_MultipleNaN_ContinuesWithLastValid()
{
int[] periods = { 5, 10 };
var wmaVector = new WmaVector(periods);
wmaVector.Update(new TValue(DateTime.UtcNow, 100.0));
wmaVector.Update(new TValue(DateTime.UtcNow, 110.0));
wmaVector.Update(new TValue(DateTime.UtcNow, 120.0));
var r1 = wmaVector.Update(new TValue(DateTime.UtcNow, double.NaN));
var r2 = wmaVector.Update(new TValue(DateTime.UtcNow, double.NaN));
var r3 = wmaVector.Update(new TValue(DateTime.UtcNow, double.NaN));
foreach (var result in r1) Assert.True(double.IsFinite(result.Value));
foreach (var result in r2) Assert.True(double.IsFinite(result.Value));
foreach (var result in r3) Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Calculate_Series_HandlesNaN()
{
int[] periods = { 5, 10 };
var wmaVector = new WmaVector(periods);
var t = new System.Collections.Generic.List<long>();
var v = new System.Collections.Generic.List<double>();
var now = DateTime.UtcNow;
t.Add(now.Ticks); v.Add(100.0);
t.Add(now.AddMinutes(1).Ticks); v.Add(110.0);
t.Add(now.AddMinutes(2).Ticks); v.Add(double.NaN);
t.Add(now.AddMinutes(3).Ticks); v.Add(120.0);
t.Add(now.AddMinutes(4).Ticks); v.Add(double.PositiveInfinity);
t.Add(now.AddMinutes(5).Ticks); v.Add(130.0);
var series = new TSeries(t, v);
var results = wmaVector.Calculate(series);
foreach (var periodResults in results)
{
foreach (var val in periodResults.Values)
{
Assert.True(double.IsFinite(val), $"Expected finite value but got {val}");
}
}
}
[Fact]
public void Reset_ClearsLastValidValue()
{
int[] periods = { 10 };
var wmaVector = new WmaVector(periods);
wmaVector.Update(new TValue(DateTime.UtcNow, 100.0));
wmaVector.Update(new TValue(DateTime.UtcNow, double.NaN));
wmaVector.Reset();
var result = wmaVector.Update(new TValue(DateTime.UtcNow, 50.0));
Assert.Equal(50.0, result[0].Value, 1e-9);
}
[Fact]
public void NaN_Handling_MatchesSingleWma()
{
int[] periods = { 5, 10, 20 };
var wmaVector = new WmaVector(periods);
var wmaSingles = periods.Select(p => new Wma(p)).ToArray();
var values = new double[] { 10, 20, double.NaN, 40, double.PositiveInfinity, 60, 70 };
var time = DateTime.UtcNow;
foreach (var val in values)
{
var tVal = new TValue(time, val);
var multiRes = wmaVector.Update(tVal);
for (int i = 0; i < periods.Length; i++)
{
var singleRes = wmaSingles[i].Update(tVal);
Assert.Equal(singleRes.Value, multiRes[i].Value, 1e-9);
}
time = time.AddMinutes(1);
}
}
[Fact]
public void Values_Property_UpdatesAfterUpdate()
{
int[] periods = { 5, 10 };
var wmaVector = new WmaVector(periods);
var result = wmaVector.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.Equal(result[0].Value, wmaVector.Values[0].Value);
Assert.Equal(result[1].Value, wmaVector.Values[1].Value);
}
[Fact]
public void Values_Property_UpdatesAfterCalculate()
{
int[] periods = { 5, 10 };
var wmaVector = new WmaVector(periods);
var t = new System.Collections.Generic.List<long> { 100, 200, 300 };
var v = new System.Collections.Generic.List<double> { 10.0, 20.0, 30.0 };
var series = new TSeries(t, v);
var results = wmaVector.Calculate(series);
Assert.Equal(results[0].Last.Value, wmaVector.Values[0].Value, 1e-9);
Assert.Equal(results[1].Last.Value, wmaVector.Values[1].Value, 1e-9);
}
[Fact]
public void Update_BarCorrection_WorksCorrectly()
{
int[] periods = { 3 };
var wmaVector = new WmaVector(periods);
wmaVector.Update(new TValue(DateTime.UtcNow, 10.0), isNew: true);
wmaVector.Update(new TValue(DateTime.UtcNow, 20.0), isNew: true);
wmaVector.Update(new TValue(DateTime.UtcNow, 30.0), isNew: true);
// WMA(3) of 10,20,30 = (1*10 + 2*20 + 3*30) / 6 = 140/6 = 23.333...
var res1 = wmaVector.Values[0].Value;
Assert.Equal(140.0 / 6.0, res1, 1e-9);
// Correct the last bar to 60
var res2 = wmaVector.Update(new TValue(DateTime.UtcNow, 60.0), isNew: false);
// WMA(3) of 10,20,60 = (1*10 + 2*20 + 3*60) / 6 = (10 + 40 + 180) / 6 = 230/6 = 38.333...
Assert.Equal(230.0 / 6.0, res2[0].Value, 1e-9);
}
[Fact]
public void WMA_MatchesExpectedValues()
{
int[] periods = { 3 };
var wmaVector = new WmaVector(periods);
// Test sequence: 10, 20, 30, 40, 50
// WMA(3) weights: [1, 2, 3], divisor = 6
// Bar 1: 10 (only value) = 10
// Bar 2: (1*10 + 2*20) / 3 = 50/3 = 16.666...
// Bar 3: (1*10 + 2*20 + 3*30) / 6 = 140/6 = 23.333...
// Bar 4: (1*20 + 2*30 + 3*40) / 6 = 200/6 = 33.333...
// Bar 5: (1*30 + 2*40 + 3*50) / 6 = 260/6 = 43.333...
double[] expected = [10.0, 50.0/3.0, 140.0/6.0, 200.0/6.0, 260.0/6.0];
var values = new double[] { 10, 20, 30, 40, 50 };
var time = DateTime.UtcNow;
for (int i = 0; i < values.Length; i++)
{
var res = wmaVector.Update(new TValue(time, values[i]));
Assert.Equal(expected[i], res[0].Value, 1e-9);
time = time.AddMinutes(1);
}
}
[Fact]
public void WMA_MoreWeightOnRecentValues()
{
int[] periods = { 3 };
var wmaVector = new WmaVector(periods);
var smaVector = new SmaVector(periods);
var values = new double[] { 10, 20, 100 }; // High recent value
var time = DateTime.UtcNow;
TValue[] wmaRes = null!;
TValue[] smaRes = null!;
foreach (var val in values)
{
var tVal = new TValue(time, val);
wmaRes = wmaVector.Update(tVal);
smaRes = smaVector.Update(tVal);
time = time.AddMinutes(1);
}
// WMA should be higher than SMA because it weights the high recent value more
// SMA = (10 + 20 + 100) / 3 = 43.333...
// WMA = (1*10 + 2*20 + 3*100) / 6 = (10 + 40 + 300) / 6 = 58.333...
Assert.True(wmaRes[0].Value > smaRes[0].Value);
Assert.Equal(350.0 / 6.0, wmaRes[0].Value, 1e-9);
Assert.Equal(130.0 / 3.0, smaRes[0].Value, 1e-9);
}
}
-264
View File
@@ -1,264 +0,0 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// Multi-Period Weighted Moving Average (WMA) - O(1) optimized per period.
/// Calculates multiple WMAs with different periods for the same input series.
/// Uses dual running sums for O(1) complexity per update per period.
/// Uses last-value substitution for invalid inputs (NaN/Infinity).
/// </summary>
[SkipLocalsInit]
public class WmaVector
{
private readonly int[] _periods;
private readonly double[] _divisors;
private readonly RingBuffer[] _buffers;
private readonly double[] _sums; // Simple sums for each period
private readonly double[] _wsums; // Weighted sums for each period
private readonly double[] _p_sums; // Saved simple sums for bar correction
private readonly double[] _p_wsums; // Saved weighted sums for bar correction
private readonly double[] _p_lastInputs; // Last inputs for bar correction
private readonly int _count;
private double _lastValidValue;
private double _p_lastValidValue;
/// <summary>
/// Current WMA values for all periods.
/// </summary>
public ReadOnlySpan<TValue> Values => _values;
private readonly TValue[] _values;
/// <summary>
/// Initializes WmaVector with specified periods.
/// </summary>
/// <param name="periods">Array of periods (each must be > 0)</param>
public WmaVector(int[] periods)
{
_count = periods.Length;
_periods = new int[_count];
_divisors = new double[_count];
_buffers = new RingBuffer[_count];
_sums = new double[_count];
_wsums = new double[_count];
_p_sums = new double[_count];
_p_wsums = new double[_count];
_p_lastInputs = new double[_count];
_values = new TValue[_count];
for (int i = 0; i < _count; i++)
{
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(periods[i], 0);
_periods[i] = periods[i];
_divisors[i] = periods[i] * (periods[i] + 1) * 0.5;
_buffers[i] = new RingBuffer(periods[i]);
}
}
/// <summary>
/// Gets a valid input value, using last-value substitution for non-finite inputs.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidValue(double input)
{
if (double.IsFinite(input))
{
_lastValidValue = input;
return input;
}
return _lastValidValue;
}
/// <summary>
/// Resets all WMA states.
/// </summary>
public void Reset()
{
for (int i = 0; i < _count; i++)
{
_buffers[i].Clear();
_sums[i] = 0;
_wsums[i] = 0;
_p_sums[i] = 0;
_p_wsums[i] = 0;
_p_lastInputs[i] = 0;
}
_lastValidValue = 0;
_p_lastValidValue = 0;
Array.Clear(_values);
}
/// <summary>
/// Updates WMAs with the given value.
/// Uses last-value substitution: invalid inputs (NaN/Infinity) are replaced with
/// the last known good value, providing continuity in the output series.
/// O(1) complexity per period using dual running sums.
/// </summary>
/// <param name="input">Input value</param>
/// <param name="isNew">True for new bar, false for update to current bar (default: true)</param>
/// <returns>Array of WMA values</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue[] Update(TValue input, bool isNew = true)
{
if (isNew)
{
// Get valid value (this may update _lastValidValue)
double val = GetValidValue(input.Value);
for (int i = 0; i < _count; i++)
{
int period = _periods[i];
var buffer = _buffers[i];
if (buffer.IsFull)
{
// Buffer is full: O(1) update using dual running sums
double oldSum = _sums[i];
double oldest = buffer.Oldest;
_sums[i] = _sums[i] - oldest + val;
_wsums[i] = _wsums[i] - oldSum + (period * val);
}
else
{
// Warmup phase: incrementally build sums
int count = buffer.Count + 1;
_sums[i] += val;
_wsums[i] += count * val;
}
buffer.Add(val);
// Save state AFTER this update for potential future corrections
_p_sums[i] = _sums[i];
_p_wsums[i] = _wsums[i];
_p_lastInputs[i] = val;
// Calculate WMA
double currentDivisor = buffer.IsFull ? _divisors[i] : buffer.Count * (buffer.Count + 1) * 0.5;
_values[i] = new TValue(input.Time, _wsums[i] / currentDivisor);
}
_p_lastValidValue = _lastValidValue;
}
else
{
// Bar correction: restore to state AFTER last isNew=true, then swap last value
_lastValidValue = _p_lastValidValue;
double val = GetValidValue(input.Value);
for (int i = 0; i < _count; i++)
{
int period = _periods[i];
var buffer = _buffers[i];
// Restore sums to state after last isNew=true
_sums[i] = _p_sums[i];
_wsums[i] = _p_wsums[i];
// Correction: replace _p_lastInputs[i] with val
int weight = buffer.IsFull ? period : buffer.Count;
_sums[i] = _sums[i] - _p_lastInputs[i] + val;
_wsums[i] += weight * (val - _p_lastInputs[i]);
buffer.UpdateNewest(val);
// Calculate WMA
double currentDivisor = buffer.IsFull ? _divisors[i] : buffer.Count * (buffer.Count + 1) * 0.5;
_values[i] = new TValue(input.Time, _wsums[i] / currentDivisor);
}
}
return _values;
}
/// <summary>
/// Calculates WMAs for the entire series.
/// </summary>
/// <param name="source">Input series</param>
/// <returns>Array of WMA series</returns>
public TSeries[] Calculate(TSeries source)
{
int len = source.Count;
var resultSeries = new TSeries[_count];
// Reset state for fresh calculation
Reset();
// Pre-allocate lists
var tLists = new List<long>[_count];
var vLists = new List<double>[_count];
for (int i = 0; i < _count; i++)
{
tLists[i] = new List<long>(len);
vLists[i] = new List<double>(len);
CollectionsMarshal.SetCount(tLists[i], len);
CollectionsMarshal.SetCount(vLists[i], len);
}
var sourceValues = source.Values;
var sourceTimes = source.Times;
for (int t = 0; t < len; t++)
{
double val = sourceValues[t];
long time = sourceTimes[t];
// Last-value substitution: replace non-finite inputs with last valid value
val = GetValidValue(val);
for (int i = 0; i < _count; i++)
{
int period = _periods[i];
var buffer = _buffers[i];
if (buffer.IsFull)
{
// Buffer is full: O(1) update
double oldSum = _sums[i];
double oldest = buffer.Oldest;
_sums[i] = _sums[i] - oldest + val;
_wsums[i] = _wsums[i] - oldSum + (period * val);
}
else
{
// Warmup phase
int count = buffer.Count + 1;
_sums[i] += val;
_wsums[i] += count * val;
}
buffer.Add(val);
CollectionsMarshal.AsSpan(tLists[i])[t] = time;
double currentDivisor = buffer.IsFull ? _divisors[i] : buffer.Count * (buffer.Count + 1) * 0.5;
CollectionsMarshal.AsSpan(vLists[i])[t] = _wsums[i] / currentDivisor;
}
}
// Create TSeries and update Values
for (int i = 0; i < _count; i++)
{
resultSeries[i] = new TSeries(tLists[i], vLists[i]);
var lastT = CollectionsMarshal.AsSpan(tLists[i])[len - 1];
var lastV = CollectionsMarshal.AsSpan(vLists[i])[len - 1];
_values[i] = new TValue(lastT, lastV);
}
return resultSeries;
}
/// <summary>
/// Calculates WMAs for the entire series using specified periods.
/// </summary>
/// <param name="source">Input series</param>
/// <param name="periods">Array of periods</param>
/// <returns>Array of WMA series</returns>
public static TSeries[] Calculate(TSeries source, int[] periods)
{
var wmaVector = new WmaVector(periods);
return wmaVector.Calculate(source);
}
}