mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 21:18:04 +00:00
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:
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user