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 -78
View File
@@ -16,7 +16,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 EMAs simultaneously.
4. **Handling Invalid Values**: Last-value substitution for NaN/Infinity.
#!csharp
@@ -165,63 +165,9 @@ Console.WriteLine($"Streaming Last Value: {lastStreamVal.Value:F2}");
#!markdown
## 4. Vectorized EMA (Multiple Periods)
## 4. Handling Invalid Values (NaN/Infinity)
`EmaVector` allows calculating multiple EMAs (e.g., 9, 12, 26) simultaneously. This is optimized for performance using SIMD where available.
### Vectorized Batch
#!csharp
int[] periods = { 9, 12, 26 };
Console.WriteLine($"\n--- Vectorized Batch EMA (Periods: {string.Join(", ", periods)}) ---");
var emaVectorBatch = new EmaVector(periods);
var vectorBatchResults = emaVectorBatch.Calculate(closeSeries);
for (int i = 0; i < periods.Length; i++)
{
Console.WriteLine($"EMA({periods[i]}) Last Value: {vectorBatchResults[i].Last().Value:F2}");
}
#!markdown
### Vectorized Streaming
#!csharp
Console.WriteLine($"\n--- Vectorized Streaming EMA (Periods: {string.Join(", ", periods)}) ---");
var emaVectorStream = new EmaVector(periods);
TValue[] lastVectorVal = null;
foreach(var item in closeSeries)
{
lastVectorVal = emaVectorStream.Update(item);
}
for (int i = 0; i < periods.Length; i++)
{
Console.WriteLine($"EMA({periods[i]}) Last Value: {lastVectorVal[i].Value:F2}");
}
// 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 `Ema` and `EmaVector` 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.
`Ema` 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
@@ -271,24 +217,3 @@ for (int i = 0; i < seriesWithNaN.Count; i++)
var inputStr = double.IsFinite(input) ? input.ToString("F2") : input.ToString();
Console.WriteLine($" {inputStr,-10} → {output:F2} (IsFinite: {double.IsFinite(output)})");
}
#!csharp
Console.WriteLine("\n--- Vectorized EMA with Invalid Values ---");
int[] periodsNaN = { 5, 10 };
var emaVectorNaN = new EmaVector(periodsNaN);
// Feed values including invalid ones
var inputsNaN = new double[] { 100, 110, double.NaN, 120, double.PositiveInfinity, 130 };
var time = DateTime.Now;
foreach (var val in inputsNaN)
{
var results = emaVectorNaN.Update(new TValue(time, val));
var inputStr = double.IsFinite(val) ? val.ToString("F2") : val.ToString();
Console.WriteLine($"Input: {inputStr,-10} → EMA(5): {results[0].Value:F2}, EMA(10): {results[1].Value:F2}");
time = time.AddMinutes(1);
}
Console.WriteLine("\nAll outputs are finite - invalid inputs were substituted with last valid values.");
+5 -63
View File
@@ -25,30 +25,18 @@ namespace QuanTAlib;
/// </remarks>
public class Ema
{
private struct State : IEquatable<State>
private struct State
{
public double Ema;
public double E; // Compensator: decays from 1.0 to 1e-10 for bias correction
public bool IsHot; // True when 95% coverage reached (E <= 0.05)
public bool IsCompensated; // True when compensator fully decayed (E <= 1e-10)
public double E;
public bool IsHot;
public bool IsCompensated;
public static State New() => new() { Ema = 0, E = 1.0, IsHot = false, IsCompensated = false };
public readonly bool Equals(State other) =>
Ema == other.Ema && E == other.E && IsHot == other.IsHot && IsCompensated == other.IsCompensated;
public override readonly bool Equals(object? obj) =>
obj is State other && Equals(other);
public override readonly int GetHashCode() =>
HashCode.Combine(Ema, E, IsHot, IsCompensated);
public static bool operator ==(State left, State right) => left.Equals(right);
public static bool operator !=(State left, State right) => !left.Equals(right);
}
private readonly double _alpha;
private readonly double _decay; // Pre-calculated (1.0 - alpha) to avoid subtraction per tick
private readonly double _decay;
private State _state = State.New();
private State _p_state = State.New();
private double _lastValidValue;
@@ -97,9 +85,6 @@ public class Ema
/// </summary>
public bool IsHot => _state.IsHot;
/// <summary>
/// Gets a valid input value, using last-value substitution for non-finite inputs.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidValue(double input)
{
@@ -111,17 +96,9 @@ public class Ema
return _lastValidValue;
}
// 95% coverage threshold: E = 1 - 0.95 = 0.05
private const double COVERAGE_THRESHOLD = 0.05;
// Compensator decay threshold for bias correction
private const double COMPENSATOR_THRESHOLD = 1e-10;
/// <summary>
/// Core EMA calculation kernel.
/// Assumes input has already been validated via GetValidValue().
/// IsHot becomes true at 95% coverage (E <= 0.05).
/// Bias correction continues until compensator decays to 1e-10.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double Compute(double input, double alpha, double decay, ref State state)
{
@@ -132,11 +109,9 @@ public class Ema
{
state.E *= decay;
// IsHot triggers at 95% coverage
if (!state.IsHot && state.E <= COVERAGE_THRESHOLD)
state.IsHot = true;
// Continue bias correction until compensator fully decays
if (state.E <= COMPENSATOR_THRESHOLD)
{
state.IsCompensated = true;
@@ -155,9 +130,6 @@ public class Ema
return result;
}
/// <summary>
/// Core calculation kernel that handles both batch and streaming-continuation.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CalculateCore(ReadOnlySpan<double> source, Span<double> output, double alpha, ref State state, ref double lastValidValue)
{
@@ -165,8 +137,6 @@ public class Ema
double decay = 1.0 - alpha;
int i = 0;
// Phase 1: Warmup with bias correction
// If state is already compensated, this loop is skipped
if (!state.IsCompensated)
{
for (; i < len && state.E > COMPENSATOR_THRESHOLD; i++)
@@ -189,7 +159,6 @@ public class Ema
state.IsCompensated = true;
}
// Phase 2: Hot loop
for (; i < len; i++)
{
double val = source[i];
@@ -203,12 +172,6 @@ public class Ema
}
}
/// <summary>
/// Updates EMA with the given value.
/// </summary>
/// <param name="input">Input value</param>
/// <param name="isNew">True for new bar, false for update to current bar (default: true)</param>
/// <returns>Compensated EMA value</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
{
@@ -221,19 +184,12 @@ public class Ema
_state = _p_state;
}
// Last-value substitution: replace non-finite inputs with last valid value
double val = GetValidValue(input.Value);
val = Compute(val, _alpha, _decay, ref _state);
Value = new TValue(input.Time, val);
return Value;
}
/// <summary>
/// Updates EMA with the entire series.
/// Uses split-loop optimization: warmup phase with bias correction, then branchless hot loop.
/// </summary>
/// <param name="source">Input series</param>
/// <returns>EMA series</returns>
public TSeries Update(TSeries source)
{
if (source.Count == 0) return new TSeries(new List<long>(), new List<double>());
@@ -249,9 +205,6 @@ public class Ema
var sourceValues = source.Values;
var sourceTimes = source.Times;
// 1. Fast Batch Calculation
// Uses the unified CalculateCore to handle both new and continuing states
// Optimization: Copy state to locals to allow JIT register allocation
State state = _state;
double lastValidValue = _lastValidValue;
@@ -260,7 +213,6 @@ public class Ema
_state = state;
_lastValidValue = lastValidValue;
// Copy Times
sourceTimes.CopyTo(tSpan);
_p_state = _state;
@@ -299,15 +251,6 @@ public class Ema
Calculate(source, output, alpha);
}
/// <summary>
/// Calculates EMA in-place using alpha, writing results to pre-allocated output span.
/// Zero-allocation method for maximum performance.
/// Bias correction continues until compensator decays to 1e-10.
/// Uses split-loop optimization: warmup phase with bias correction, then branchless hot loop.
/// </summary>
/// <param name="source">Input values</param>
/// <param name="output">Output span (must be same length as source)</param>
/// <param name="alpha">Smoothing factor (0 < alpha <= 1)</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, double alpha)
{
@@ -318,7 +261,6 @@ public class Ema
if (source.Length == 0) return;
// Initialize default state for static calculation
State state = State.New();
double lastValid = 0;
+1 -27
View File
@@ -115,33 +115,9 @@ Console.WriteLine($"Last EMA: {emaOutput[^1]}");
* **Hunter's bias correction**: Same accuracy as TSeries API
* **Compatible** with `ArrayPool<T>` for buffer management
### Multi-Alpha EMA (`EmaVector`)
The `EmaVector` class is a SIMD-optimized implementation for calculating multiple EMAs with different periods on the same input series simultaneously. It leverages hardware intrinsics (AVX/SSE) for high performance.
```csharp
using QuanTAlib;
// Initialize with multiple periods
int[] periods = { 9, 12, 26 };
var emaVector = new EmaVector(periods);
// Streaming update
TValue[] results = emaVector.Update(new TValue(time, price));
// Access values
Console.WriteLine($"EMA(9): {results[0].Value}");
Console.WriteLine($"EMA(12): {results[1].Value}");
Console.WriteLine($"EMA(26): {results[2].Value}");
// Batch calculation
TSeries source = ...;
TSeries[] seriesResults = emaVector.Calculate(source);
```
### Handling Invalid Values (NaN/Infinity)
Both `Ema` and `EmaVector` use **last-value substitution** for handling invalid inputs:
`Ema` uses **last-value substitution** for handling invalid inputs:
```csharp
var ema = new Ema(10);
@@ -166,13 +142,11 @@ var results = ema.Update(series); // All values are finite
* When `NaN`, `PositiveInfinity`, or `NegativeInfinity` is encountered, the last valid value is substituted
* This provides output continuity instead of propagating invalid values
* Both scalar (`Ema`) and SIMD (`EmaVector`) implementations use identical logic
* `Reset()` clears the last valid value, so the next valid input establishes a new baseline
### Performance Characteristics
* **O(1) Complexity:** The calculation time is constant regardless of the period length.
* **SIMD Optimization:** `EmaVector` processes multiple periods in parallel using vector instructions, significantly reducing CPU cycles for multi-timeframe analysis.
* **Zero Allocation:** The streaming `Update` method is designed to be allocation-free (excluding the return struct).
## Interpretation Details
-378
View File
@@ -1,378 +0,0 @@
namespace QuanTAlib.Tests;
public class EmaVectorTests
{
[Fact]
public void Initialization_WithPeriods_SetsCorrectAlphas()
{
int[] periods = { 10, 20 };
var emaVector = new EmaVector(periods);
var res = emaVector.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.Equal(100.0, res[0].Value, 1e-9);
Assert.Equal(100.0, res[1].Value, 1e-9);
}
[Fact]
public void Initialization_WithAlphas_Works()
{
double[] alphas = { 0.1, 0.2, 0.5 };
var emaVector = new EmaVector(alphas);
var res = emaVector.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 EmaVector(periods));
}
[Fact]
public void Initialization_WithNegativePeriod_ThrowsArgumentException()
{
int[] periods = { 10, -5, 20 };
Assert.Throws<ArgumentOutOfRangeException>(() => new EmaVector(periods));
}
[Fact]
public void Initialization_WithZeroAlpha_ThrowsArgumentException()
{
double[] alphas = { 0.1, 0.0, 0.5 };
Assert.Throws<ArgumentOutOfRangeException>(() => new EmaVector(alphas));
}
[Fact]
public void Initialization_WithNegativeAlpha_ThrowsArgumentException()
{
double[] alphas = { 0.1, -0.1, 0.5 };
Assert.Throws<ArgumentOutOfRangeException>(() => new EmaVector(alphas));
}
[Fact]
public void Initialization_WithAlphaGreaterThanOne_ThrowsArgumentException()
{
double[] alphas = { 0.1, 1.5, 0.5 };
Assert.Throws<ArgumentOutOfRangeException>(() => new EmaVector(alphas));
}
[Fact]
public void Initialization_WithAlphaEqualToOne_Works()
{
double[] alphas = { 0.1, 1.0, 0.5 };
var emaVector = new EmaVector(alphas);
var res = emaVector.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.Equal(3, res.Length);
}
[Fact]
public void Calc_Streaming_MatchesSingleEma()
{
int[] periods = { 5, 10, 20 };
var emaVector = new EmaVector(periods);
var emaSingles = periods.Select(p => new Ema(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 = emaVector.Update(tVal);
for (int i = 0; i < periods.Length; i++)
{
var singleRes = emaSingles[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_MatchesSingleEma()
{
int[] periods = { 5, 10, 20 };
var emaVector = new EmaVector(periods);
var emaSingles = periods.Select(p => new Ema(p)).ToArray();
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 = emaVector.Calculate(series);
for (int i = 0; i < periods.Length; i++)
{
var singleRes = emaSingles[i].Update(series);
Assert.Equal(singleRes.Count, multiRes[i].Count);
for (int j = 0; j < len; j++)
{
Assert.Equal(singleRes.Values[j], multiRes[i].Values[j], 1e-8);
}
}
}
[Fact]
public void Calc_Series_MatchesStreaming()
{
int[] periods = { 5, 10, 20 };
var emaVectorBatch = new EmaVector(periods);
var emaVectorStream = new EmaVector(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 = emaVectorBatch.Calculate(series);
for (int i = 0; i < len; i++)
{
var tVal = new TValue(new DateTime(t[i], DateTimeKind.Utc), v[i]);
var streamRes = emaVectorStream.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 instanceEma = new EmaVector(periods);
var instanceRes = instanceEma.Calculate(series);
var staticRes = EmaVector.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 emaVector = new EmaVector(periods);
emaVector.Update(new TValue(DateTime.UtcNow, 100.0));
emaVector.Reset();
var res = emaVector.Update(new TValue(DateTime.UtcNow, 200.0));
Assert.Equal(200.0, res[0].Value, 1e-9);
}
[Fact]
public void Update_NaN_Input_UsesLastValidValue()
{
int[] periods = { 10, 20 };
var emaVector = new EmaVector(periods);
emaVector.Update(new TValue(DateTime.UtcNow, 100.0));
emaVector.Update(new TValue(DateTime.UtcNow, 110.0));
var resultAfterNaN = emaVector.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 emaVector = new EmaVector(periods);
emaVector.Update(new TValue(DateTime.UtcNow, 100.0));
emaVector.Update(new TValue(DateTime.UtcNow, 110.0));
var resultAfterPosInf = emaVector.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
foreach (var result in resultAfterPosInf)
{
Assert.True(double.IsFinite(result.Value));
}
var resultAfterNegInf = emaVector.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 emaVector = new EmaVector(periods);
emaVector.Update(new TValue(DateTime.UtcNow, 100.0));
emaVector.Update(new TValue(DateTime.UtcNow, 110.0));
emaVector.Update(new TValue(DateTime.UtcNow, 120.0));
var r1 = emaVector.Update(new TValue(DateTime.UtcNow, double.NaN));
var r2 = emaVector.Update(new TValue(DateTime.UtcNow, double.NaN));
var r3 = emaVector.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 emaVector = new EmaVector(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 = emaVector.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 emaVector = new EmaVector(periods);
emaVector.Update(new TValue(DateTime.UtcNow, 100.0));
emaVector.Update(new TValue(DateTime.UtcNow, double.NaN));
emaVector.Reset();
var result = emaVector.Update(new TValue(DateTime.UtcNow, 50.0));
Assert.Equal(50.0, result[0].Value, 1e-9);
}
[Fact]
public void NaN_Handling_MatchesSingleEma()
{
int[] periods = { 5, 10, 20 };
var emaVector = new EmaVector(periods);
var emaSingles = periods.Select(p => new Ema(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 = emaVector.Update(tVal);
for (int i = 0; i < periods.Length; i++)
{
var singleRes = emaSingles[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 emaVector = new EmaVector(periods);
var result = emaVector.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.Equal(result[0].Value, emaVector.Values[0].Value);
Assert.Equal(result[1].Value, emaVector.Values[1].Value);
}
[Fact]
public void Values_Property_UpdatesAfterCalculate()
{
int[] periods = { 5, 10 };
var emaVector = new EmaVector(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 = emaVector.Calculate(series);
Assert.Equal(results[0].Last.Value, emaVector.Values[0].Value, 1e-9);
Assert.Equal(results[1].Last.Value, emaVector.Values[1].Value, 1e-9);
}
}
-327
View File
@@ -1,327 +0,0 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// Multi-Alpha Exponential Moving Average (EMA) - SIMD optimized.
/// Calculates multiple EMAs with different periods/alphas for the same input series in parallel.
/// Uses last-value substitution for invalid inputs (NaN/Infinity).
/// </summary>
[SkipLocalsInit]
public class EmaVector
{
private readonly double[] _alphas;
private readonly double[] _emas;
private readonly double[] _Es;
private readonly double[] _p_emas;
private readonly double[] _p_Es;
private readonly int _count;
private double _lastValidValue;
/// <summary>
/// Current EMA values for all periods.
/// </summary>
public ReadOnlySpan<TValue> Values => _values;
private readonly TValue[] _values;
/// <summary>
/// Initializes EmaVector with specified periods.
/// </summary>
/// <param name="periods">Array of periods</param>
public EmaVector(int[] periods)
{
_count = periods.Length;
_alphas = new double[_count];
_emas = new double[_count];
_Es = new double[_count];
_p_emas = new double[_count];
_p_Es = new double[_count];
_values = new TValue[_count];
for (int i = 0; i < _count; i++)
{
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(periods[i], 0);
_alphas[i] = 2.0 / (periods[i] + 1);
ResetAt(i);
}
}
/// <summary>
/// Initializes EmaVector with specified alphas.
/// </summary>
/// <param name="alphas">Array of alphas</param>
public EmaVector(double[] alphas)
{
_count = alphas.Length;
_alphas = new double[_count];
_emas = new double[_count];
_Es = new double[_count];
_p_emas = new double[_count];
_p_Es = new double[_count];
_values = new TValue[_count];
for (int i = 0; i < _count; i++)
{
if (alphas[i] <= 0 || alphas[i] > 1)
throw new ArgumentOutOfRangeException(nameof(alphas), alphas[i], "Alpha must be between 0 (exclusive) and 1 (inclusive)");
_alphas[i] = alphas[i];
ResetAt(i);
}
}
private void ResetAt(int index)
{
_emas[index] = 0.0;
_Es[index] = 1.0;
}
/// <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 EMA states.
/// </summary>
public void Reset()
{
for (int i = 0; i < _count; i++)
{
ResetAt(i);
}
_lastValidValue = 0;
Array.Clear(_values);
}
/// <summary>
/// Updates EMAs 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.
/// </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 compensated EMA values</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue[] Update(TValue input, bool isNew = true)
{
if (isNew)
{
Array.Copy(_emas, _p_emas, _count);
Array.Copy(_Es, _p_Es, _count);
}
else
{
Array.Copy(_p_emas, _emas, _count);
Array.Copy(_p_Es, _Es, _count);
}
// Last-value substitution: replace non-finite inputs with last valid value
double val = GetValidValue(input.Value);
// SIMD Loop
int vecCount = Vector<double>.Count;
int i = 0;
if (Vector.IsHardwareAccelerated && _count >= vecCount)
{
var vecInput = new Vector<double>(val);
var vecOne = Vector<double>.One;
var vecEpsilon = new Vector<double>(1e-10);
for (; i <= _count - vecCount; i += vecCount)
{
// Load state
var vecAlpha = new Vector<double>(_alphas, i);
var vecEma = new Vector<double>(_emas, i);
var vecE = new Vector<double>(_Es, i);
// Update EMA
// ema += alpha * (input - ema)
vecEma += vecAlpha * (vecInput - vecEma);
// Update E (warmup factor)
// E *= (1 - alpha)
vecE *= (vecOne - vecAlpha);
// Calculate compensated result
// res = ema / (1 - E)
var vecCompensated = vecEma / (vecOne - vecE);
// Check warmup condition: E <= 1e-10 means "hot" (use raw EMA)
// Vector.LessThanOrEqual returns Vector<long> with all-1s for true, all-0s for false
// We reinterpret as Vector<double> for use with ConditionalSelect
var isHotMask = Vector.LessThanOrEqual(vecE, vecEpsilon);
// Select result: if hot (E <= epsilon), use raw EMA; otherwise use compensated
// ConditionalSelect: mask=true -> first arg, mask=false -> second arg
var vecResult = Vector.ConditionalSelect(
Vector.AsVectorDouble(isHotMask),
vecEma, // Hot: use raw EMA
vecCompensated // Cold: use compensated
);
// Store state
vecEma.CopyTo(_emas, i);
vecE.CopyTo(_Es, i);
// Store result
for (int j = 0; j < vecCount; j++)
{
_values[i + j] = new TValue(input.Time, vecResult[j]);
}
}
}
// Scalar fallback for remaining items
for (; i < _count; i++)
{
double alpha = _alphas[i];
_emas[i] += alpha * (val - _emas[i]);
double result = _emas[i];
if (_Es[i] > 1e-10)
{
_Es[i] *= (1.0 - alpha);
if (_Es[i] > 1e-10)
{
result = _emas[i] / (1.0 - _Es[i]);
}
}
_values[i] = new TValue(input.Time, result);
}
return _values;
}
/// <summary>
/// Calculates EMAs for the entire series.
/// </summary>
/// <param name="source">Input series</param>
/// <returns>Array of EMA series</returns>
public TSeries[] Calculate(TSeries source)
{
int len = source.Count;
var resultSeries = new TSeries[_count];
// 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;
int vecCount = Vector<double>.Count;
var vecOne = Vector<double>.One;
var vecEpsilon = new Vector<double>(1e-10);
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);
var vecInput = new Vector<double>(val);
int i = 0;
if (Vector.IsHardwareAccelerated && _count >= vecCount)
{
for (; i <= _count - vecCount; i += vecCount)
{
var vecAlpha = new Vector<double>(_alphas, i);
var vecEma = new Vector<double>(_emas, i);
var vecE = new Vector<double>(_Es, i);
vecEma += vecAlpha * (vecInput - vecEma);
vecE *= (vecOne - vecAlpha);
var vecCompensated = vecEma / (vecOne - vecE);
// Check warmup condition: E <= 1e-10 means "hot" (use raw EMA)
var isHotMask = Vector.LessThanOrEqual(vecE, vecEpsilon);
// Select result: if hot, use raw EMA; otherwise use compensated
var vecResult = Vector.ConditionalSelect(
Vector.AsVectorDouble(isHotMask),
vecEma, // Hot: use raw EMA
vecCompensated // Cold: use compensated
);
vecEma.CopyTo(_emas, i);
vecE.CopyTo(_Es, i);
// Scatter results to lists
for (int j = 0; j < vecCount; j++)
{
CollectionsMarshal.AsSpan(tLists[i + j])[t] = time;
CollectionsMarshal.AsSpan(vLists[i + j])[t] = vecResult[j];
}
}
}
for (; i < _count; i++)
{
double alpha = _alphas[i];
_emas[i] += alpha * (val - _emas[i]);
double result = _emas[i];
if (_Es[i] > 1e-10)
{
_Es[i] *= (1.0 - alpha);
if (_Es[i] > 1e-10)
{
result = _emas[i] / (1.0 - _Es[i]);
}
}
CollectionsMarshal.AsSpan(tLists[i])[t] = time;
CollectionsMarshal.AsSpan(vLists[i])[t] = result;
}
}
// 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 EMAs for the entire series using specified periods.
/// </summary>
/// <param name="source">Input series</param>
/// <param name="periods">Array of periods</param>
/// <returns>Array of EMA series</returns>
public static TSeries[] Calculate(TSeries source, int[] periods)
{
var emaVector = new EmaVector(periods);
return emaVector.Calculate(source);
}
}
+5 -79
View File
@@ -21,7 +21,8 @@ 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 SMAs simultaneously.
4. **Handling Invalid Values**: Last-value substitution for NaN/Infinity.
5. **SMA vs EMA**: Comparing Simple and Exponential Moving Averages.
#!csharp
@@ -189,63 +190,9 @@ Console.WriteLine($"Match: {Math.Abs(batchLargeResult.Last().Value - lastStreamV
#!markdown
## 4. Vectorized SMA (Multiple Periods)
## 4. Handling Invalid Values (NaN/Infinity)
`SmaVector` allows calculating multiple SMAs (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 SMA (Periods: {string.Join(", ", periods)}) ---");
var smaVectorBatch = new SmaVector(periods);
var vectorBatchResults = smaVectorBatch.Calculate(closeSeries);
for (int i = 0; i < periods.Length; i++)
{
Console.WriteLine($"SMA({periods[i]}) Last Value: {vectorBatchResults[i].Last().Value:F2}");
}
#!markdown
### Vectorized Streaming
#!csharp
Console.WriteLine($"\n--- Vectorized Streaming SMA (Periods: {string.Join(", ", periods)}) ---");
var smaVectorStream = new SmaVector(periods);
TValue[] lastVectorVal = null;
foreach(var item in closeSeries)
{
lastVectorVal = smaVectorStream.Update(item);
}
for (int i = 0; i < periods.Length; i++)
{
Console.WriteLine($"SMA({periods[i]}) Last Value: {lastVectorVal[i].Value:F2}");
}
// 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 `Sma` and `SmaVector` 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.
`Sma` 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
@@ -296,30 +243,9 @@ for (int i = 0; i < seriesWithNaN.Count; i++)
Console.WriteLine($" {inputStr,-10} → {output:F2} (IsFinite: {double.IsFinite(output)})");
}
#!csharp
Console.WriteLine("\n--- Vectorized SMA with Invalid Values ---");
int[] periodsNaN = { 5, 10 };
var smaVectorNaN = new SmaVector(periodsNaN);
// Feed values including invalid ones
var inputsNaN = new double[] { 100, 110, double.NaN, 120, double.PositiveInfinity, 130 };
var time = DateTime.Now;
foreach (var val in inputsNaN)
{
var results = smaVectorNaN.Update(new TValue(time, val));
var inputStr = double.IsFinite(val) ? val.ToString("F2") : val.ToString();
Console.WriteLine($"Input: {inputStr,-10} → SMA(5): {results[0].Value:F2}, SMA(10): {results[1].Value:F2}");
time = time.AddMinutes(1);
}
Console.WriteLine("\nAll outputs are finite - invalid inputs were substituted with last valid values.");
#!markdown
## 6. SMA vs EMA Comparison
## 5. SMA vs EMA Comparison
The SMA and EMA are both trend-following indicators, but they weight data differently:
+7 -93
View File
@@ -38,15 +38,13 @@ public sealed class Sma
private readonly int _period;
private readonly RingBuffer _buffer;
// Running sum maintained separately for O(1) bar correction
private double _sum;
private double _p_sum; // Sum AFTER last isNew=true (for correction restore)
private double _p_lastInput; // Input that was added on last isNew=true
private double _p_sum;
private double _p_lastInput;
private double _lastValidValue;
private double _p_lastValidValue;
private int _tickCount; // Counter for periodic sum resync
private int _tickCount;
// Resync interval: recalculate sum from buffer every N ticks to prevent drift
private const int ResyncInterval = 1000;
/// <summary>
@@ -93,23 +91,15 @@ public sealed class Sma
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)
{
// Calculate what to remove from sum (oldest value if buffer full)
double removedValue = _buffer.Count == _buffer.Capacity ? _buffer.Oldest : 0.0;
// Update sum: remove oldest, add newest
_sum = _sum - removedValue + val;
// Update buffer
_buffer.Add(val);
// Periodic resync: recalculate sum from scratch to eliminate floating-point drift
_tickCount++;
if (_buffer.IsFull && _tickCount >= ResyncInterval)
{
@@ -118,43 +108,27 @@ public sealed class Sma
}
}
/// <summary>
/// Updates SMA 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 SMA 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_lastInput = val;
_p_lastValidValue = _lastValidValue;
}
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);
// _p_sum is the sum AFTER the last isNew=true completed
// _p_lastInput is the value that was added on last isNew=true
// We want: new_sum = _p_sum - _p_lastInput + val
_sum = _p_sum - _p_lastInput + val;
// Update buffer's newest value
_buffer.UpdateNewest(val);
}
@@ -163,11 +137,6 @@ public sealed class Sma
return Value;
}
/// <summary>
/// Updates SMA with the entire series.
/// </summary>
/// <param name="source">Input series</param>
/// <returns>SMA series</returns>
public TSeries Update(TSeries source)
{
if (source.Count == 0) return new TSeries(new List<long>(), new List<double>());
@@ -183,25 +152,15 @@ public sealed class Sma
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, and _lastValidValue to what they would be
// if we had processed the series sequentially.
// Find the last valid value before the reconstruction window
// The reconstruction window is the last 'period' elements (or less if len < period)
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]))
@@ -213,10 +172,9 @@ public sealed class Sma
}
else
{
_lastValidValue = 0; // Reset if starting from 0
_lastValidValue = 0;
}
// Rebuild buffer and sum from last 'period' values using shared logic
_buffer.Clear();
_sum = 0;
_tickCount = 0;
@@ -227,7 +185,6 @@ public sealed class Sma
UpdateState(val);
}
// Save state for potential future corrections
_p_sum = _sum;
_p_lastInput = sourceValues[len - 1];
_p_lastValidValue = _lastValidValue;
@@ -281,17 +238,11 @@ public sealed class Sma
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)
{
int len = source.Length;
// Use stackalloc for small periods, otherwise fall back to heap allocation
const int StackAllocThreshold = 256;
Span<double> buffer = period <= StackAllocThreshold
? stackalloc double[period]
@@ -302,8 +253,6 @@ public sealed class Sma
int bufferIndex = 0;
int i = 0;
// Phase 1: Warmup (0 to period-1)
// No need to remove oldest value, just accumulate
int warmupEnd = Math.Min(period, len);
for (; i < warmupEnd; i++)
{
@@ -318,9 +267,6 @@ public sealed class Sma
output[i] = sum / (i + 1);
}
// Phase 2: Hot loop (period to len)
// Buffer is full, remove oldest, add newest
// Optimized buffer indexing (no modulo)
int tickCount = 0;
for (; i < len; i++)
{
@@ -330,23 +276,19 @@ public sealed class Sma
else
val = lastValid;
// Remove oldest, add newest
sum = sum - buffer[bufferIndex] + val;
buffer[bufferIndex] = val;
// Increment buffer index with wrap-around check (faster than modulo)
bufferIndex++;
if (bufferIndex >= period)
bufferIndex = 0;
output[i] = sum / period;
// Periodic resync every 1000 ticks
tickCount++;
if (tickCount >= ResyncInterval)
{
tickCount = 0;
// Recalculate sum from buffer to prevent drift
double recalcSum = 0;
for (int k = 0; k < period; k++)
{
@@ -357,29 +299,17 @@ public sealed class Sma
}
}
/// <summary>
/// SIMD-optimized implementation for SMA calculation.
/// Processes 4 consecutive values per iteration using AVX2 (Vector256&lt;double&gt;).
/// Assumes input contains no NaN/Infinity values.
/// </summary>
/// <remarks>
/// Key insight: For consecutive positions i, i+1, i+2, i+3:
/// - sum[i+1] = sum[i] - src[i-period+1] + src[i+1]
/// - We can vectorize the load of 4 "leaving" values and 4 "entering" values
/// - Then use prefix-sum style to compute the 4 sums from one base sum
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private static unsafe void CalculateSimdCore(ReadOnlySpan<double> source, Span<double> output, int period)
{
int len = source.Length;
const int VectorWidth = 4; // Vector256<double> holds 4 doubles
const int VectorWidth = 4;
fixed (double* srcPtr = source)
fixed (double* outPtr = output)
{
double invPeriod = 1.0 / period;
// Phase 1: Warmup - scalar processing until buffer is full
int warmupEnd = Math.Min(period, len);
double sum = 0;
for (int i = 0; i < warmupEnd; i++)
@@ -391,8 +321,6 @@ public sealed class Sma
if (len <= period)
return;
// Phase 2: SIMD hot loop
// Uses prefix-sum approach to break dependency chain
var vInvPeriod = Vector256.Create(invPeriod);
var vZero = Vector256<double>.Zero;
int simdEnd = period + ((len - period) / VectorWidth) * VectorWidth;
@@ -400,44 +328,31 @@ public sealed class Sma
for (int i = period; i < simdEnd; i += VectorWidth)
{
// Load 4 entering values and 4 leaving values
var vNew = Avx.LoadVector256(srcPtr + i);
var vOld = Avx.LoadVector256(srcPtr + i - period);
// Delta = New - Old
var vDelta = Avx.Subtract(vNew, vOld);
// Prefix sum of Deltas
// Step 1: Shift right by 1 element (insert 0)
// [D0, D1, D2, D3] -> [0, D0, D1, D2]
var vShift1 = Avx2.Permute4x64(vDelta.AsUInt64(), 0b_10_01_00_00).AsDouble();
vShift1 = Avx.Blend(vZero, vShift1, 0b_1110);
var vP1 = Avx.Add(vDelta, vShift1); // [D0, D0+D1, D1+D2, D2+D3]
var vP1 = Avx.Add(vDelta, vShift1);
// Step 2: Shift right by 2 elements (insert 0)
// [D0, D0+D1, D1+D2, D2+D3] -> [0, 0, D0, D0+D1]
var vShift2 = Avx2.Permute4x64(vP1.AsUInt64(), 0b_01_00_00_00).AsDouble();
vShift2 = Avx.Blend(vZero, vShift2, 0b_1100);
var vP2 = Avx.Add(vP1, vShift2); // [D0, D0+D1, D0+D1+D2, D0+D1+D2+D3]
var vP2 = Avx.Add(vP1, vShift2);
// Add previous sum to all
var vSumPrev = Vector256.Create(sum);
var vSums = Avx.Add(vSumPrev, vP2);
// Store result
var vResult = Avx.Multiply(vSums, vInvPeriod);
Avx.Store(outPtr + i, vResult);
// Update sum for next iteration (last element of vSums)
sum = vSums.GetElement(3);
// Periodic resync every 1000 ticks
tickCount += VectorWidth;
if (tickCount >= ResyncInterval)
{
tickCount = 0;
// Recalculate sum from scratch using the window ending at i + VectorWidth - 1
// Window: [i + VectorWidth - period ... i + VectorWidth - 1]
int lastIdx = i + VectorWidth - 1;
double recalcSum = 0;
for (int k = 0; k < period; k++)
@@ -448,7 +363,6 @@ public sealed class Sma
}
}
// Phase 3: Scalar tail
for (int i = simdEnd; i < len; i++)
{
sum = sum - srcPtr[i - period] + srcPtr[i];
+2 -26
View File
@@ -99,33 +99,9 @@ Console.WriteLine($"Last SMA: {smaOutput[^1]}");
* **2-3x faster** than TSeries API for large datasets
* **Compatible** with `ArrayPool<T>` for buffer management
### Multi-Period SMA (`SmaVector`)
The `SmaVector` class calculates multiple SMAs with different periods on the same input series simultaneously.
```csharp
using QuanTAlib;
// Initialize with multiple periods
int[] periods = { 5, 10, 20 };
var smaVector = new SmaVector(periods);
// Streaming update
TValue[] results = smaVector.Update(new TValue(time, price));
// Access values
Console.WriteLine($"SMA(5): {results[0].Value}");
Console.WriteLine($"SMA(10): {results[1].Value}");
Console.WriteLine($"SMA(20): {results[2].Value}");
// Batch calculation
TSeries source = ...;
TSeries[] seriesResults = smaVector.Calculate(source);
```
### Bar Correction (isNew Parameter)
Both `Sma` and `SmaVector` support intra-bar updates for real-time trading systems:
`Sma` supports intra-bar updates for real-time trading systems:
```csharp
var sma = new Sma(10);
@@ -151,7 +127,7 @@ sma.Update(new TValue(time + 1, 101.2), isNew: true);
### Handling Invalid Values (NaN/Infinity)
Both `Sma` and `SmaVector` use **last-value substitution** for handling invalid inputs:
`Sma` uses **last-value substitution** for handling invalid inputs:
```csharp
var sma = new Sma(10);
-371
View File
@@ -1,371 +0,0 @@
namespace QuanTAlib.Tests;
public class SmaVectorTests
{
[Fact]
public void Initialization_WithPeriods_Works()
{
int[] periods = { 5, 10, 20 };
var smaVector = new SmaVector(periods);
var res = smaVector.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 SmaVector(periods));
}
[Fact]
public void Initialization_WithNegativePeriod_ThrowsArgumentException()
{
int[] periods = { 10, -5, 20 };
Assert.Throws<ArgumentOutOfRangeException>(() => new SmaVector(periods));
}
[Fact]
public void Calc_Streaming_MatchesSingleSma()
{
int[] periods = { 5, 10, 20 };
var smaVector = new SmaVector(periods);
var smaSingles = periods.Select(p => new Sma(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 = smaVector.Update(tVal);
for (int i = 0; i < periods.Length; i++)
{
var singleRes = smaSingles[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_MatchesSingleSma()
{
int[] periods = { 5, 10, 20 };
var smaVector = new SmaVector(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 = smaVector.Calculate(series);
// Reset and recalculate for comparison
var smaSingles = periods.Select(p => new Sma(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 = smaSingles[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 smaVectorBatch = new SmaVector(periods);
var smaVectorStream = new SmaVector(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 = smaVectorBatch.Calculate(series);
for (int i = 0; i < len; i++)
{
var tVal = new TValue(new DateTime(t[i], DateTimeKind.Utc), v[i]);
var streamRes = smaVectorStream.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 instanceSma = new SmaVector(periods);
var instanceRes = instanceSma.Calculate(series);
var staticRes = SmaVector.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 smaVector = new SmaVector(periods);
smaVector.Update(new TValue(DateTime.UtcNow, 100.0));
smaVector.Update(new TValue(DateTime.UtcNow, 200.0));
smaVector.Reset();
var res = smaVector.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 smaVector = new SmaVector(periods);
smaVector.Update(new TValue(DateTime.UtcNow, 100.0));
smaVector.Update(new TValue(DateTime.UtcNow, 110.0));
var resultAfterNaN = smaVector.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 smaVector = new SmaVector(periods);
smaVector.Update(new TValue(DateTime.UtcNow, 100.0));
smaVector.Update(new TValue(DateTime.UtcNow, 110.0));
var resultAfterPosInf = smaVector.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
foreach (var result in resultAfterPosInf)
{
Assert.True(double.IsFinite(result.Value));
}
var resultAfterNegInf = smaVector.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 smaVector = new SmaVector(periods);
smaVector.Update(new TValue(DateTime.UtcNow, 100.0));
smaVector.Update(new TValue(DateTime.UtcNow, 110.0));
smaVector.Update(new TValue(DateTime.UtcNow, 120.0));
var r1 = smaVector.Update(new TValue(DateTime.UtcNow, double.NaN));
var r2 = smaVector.Update(new TValue(DateTime.UtcNow, double.NaN));
var r3 = smaVector.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 smaVector = new SmaVector(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 = smaVector.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 smaVector = new SmaVector(periods);
smaVector.Update(new TValue(DateTime.UtcNow, 100.0));
smaVector.Update(new TValue(DateTime.UtcNow, double.NaN));
smaVector.Reset();
var result = smaVector.Update(new TValue(DateTime.UtcNow, 50.0));
Assert.Equal(50.0, result[0].Value, 1e-9);
}
[Fact]
public void NaN_Handling_MatchesSingleSma()
{
int[] periods = { 5, 10, 20 };
var smaVector = new SmaVector(periods);
var smaSingles = periods.Select(p => new Sma(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 = smaVector.Update(tVal);
for (int i = 0; i < periods.Length; i++)
{
var singleRes = smaSingles[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 smaVector = new SmaVector(periods);
var result = smaVector.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.Equal(result[0].Value, smaVector.Values[0].Value);
Assert.Equal(result[1].Value, smaVector.Values[1].Value);
}
[Fact]
public void Values_Property_UpdatesAfterCalculate()
{
int[] periods = { 5, 10 };
var smaVector = new SmaVector(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 = smaVector.Calculate(series);
Assert.Equal(results[0].Last.Value, smaVector.Values[0].Value, 1e-9);
Assert.Equal(results[1].Last.Value, smaVector.Values[1].Value, 1e-9);
}
[Fact]
public void Update_BarCorrection_WorksCorrectly()
{
int[] periods = { 3 };
var smaVector = new SmaVector(periods);
smaVector.Update(new TValue(DateTime.UtcNow, 10.0), isNew: true);
smaVector.Update(new TValue(DateTime.UtcNow, 20.0), isNew: true);
smaVector.Update(new TValue(DateTime.UtcNow, 30.0), isNew: true);
var res1 = smaVector.Values[0].Value;
Assert.Equal(20.0, res1, 1e-9); // (10+20+30)/3 = 20
// Correct the last bar
var res2 = smaVector.Update(new TValue(DateTime.UtcNow, 60.0), isNew: false);
Assert.Equal(30.0, res2[0].Value, 1e-9); // (10+20+60)/3 = 30
}
[Fact]
public void SMA_MatchesExpectedValues()
{
int[] periods = { 3 };
var smaVector = new SmaVector(periods);
// Test sequence: 10, 20, 30, 40, 50
// Expected SMA(3): 10, 15, 20, 30, 40
var expected = new double[] { 10, 15, 20, 30, 40 };
var values = new double[] { 10, 20, 30, 40, 50 };
var time = DateTime.UtcNow;
for (int i = 0; i < values.Length; i++)
{
var res = smaVector.Update(new TValue(time, values[i]));
Assert.Equal(expected[i], res[0].Value, 1e-9);
time = time.AddMinutes(1);
}
}
}
-185
View File
@@ -1,185 +0,0 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// Multi-Period Simple Moving Average (SMA) - SIMD optimized.
/// Calculates multiple SMAs with different periods for the same input series in parallel.
/// Uses last-value substitution for invalid inputs (NaN/Infinity).
/// </summary>
[SkipLocalsInit]
public class SmaVector
{
private readonly RingBuffer[] _buffers;
private readonly RingBuffer[] _p_buffers; // Previous state for bar correction
private readonly int _count;
private double _lastValidValue;
/// <summary>
/// Current SMA values for all periods.
/// </summary>
public ReadOnlySpan<TValue> Values => _values;
private readonly TValue[] _values;
/// <summary>
/// Initializes SmaVector with specified periods.
/// </summary>
/// <param name="periods">Array of periods (each must be > 0)</param>
public SmaVector(int[] periods)
{
_count = periods.Length;
_buffers = new RingBuffer[_count];
_p_buffers = new RingBuffer[_count];
_values = new TValue[_count];
for (int i = 0; i < _count; i++)
{
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(periods[i], 0);
_buffers[i] = new RingBuffer(periods[i]);
_p_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 SMA states.
/// </summary>
public void Reset()
{
for (int i = 0; i < _count; i++)
{
_buffers[i].Clear();
_p_buffers[i].Clear();
}
_lastValidValue = 0;
Array.Clear(_values);
}
/// <summary>
/// Updates SMAs 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.
/// </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 SMA values</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue[] Update(TValue input, bool isNew = true)
{
if (isNew)
{
// Save current state for potential bar correction
for (int i = 0; i < _count; i++)
{
_p_buffers[i].CopyFrom(_buffers[i]);
}
}
else
{
// Restore previous state for bar correction
for (int i = 0; i < _count; i++)
{
_buffers[i].CopyFrom(_p_buffers[i]);
}
}
// Last-value substitution: replace non-finite inputs with last valid value
double val = GetValidValue(input.Value);
// Update each buffer and calculate SMA
for (int i = 0; i < _count; i++)
{
_buffers[i].Add(val);
_values[i] = new TValue(input.Time, _buffers[i].Average);
}
return _values;
}
/// <summary>
/// Calculates SMAs for the entire series.
/// </summary>
/// <param name="source">Input series</param>
/// <returns>Array of SMA series</returns>
public TSeries[] Calculate(TSeries source)
{
int len = source.Count;
var resultSeries = new TSeries[_count];
// Reset state for fresh calculation
for (int i = 0; i < _count; i++)
{
_buffers[i].Clear();
}
_lastValidValue = 0;
// 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++)
{
_buffers[i].Add(val);
CollectionsMarshal.AsSpan(tLists[i])[t] = time;
CollectionsMarshal.AsSpan(vLists[i])[t] = _buffers[i].Average;
}
}
// 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 SMAs for the entire series using specified periods.
/// </summary>
/// <param name="source">Input series</param>
/// <param name="periods">Array of periods</param>
/// <returns>Array of SMA series</returns>
public static TSeries[] Calculate(TSeries source, int[] periods)
{
var smaVector = new SmaVector(periods);
return smaVector.Calculate(source);
}
}
+4 -1
View File
@@ -108,7 +108,6 @@ public class TrimaTests
trima.Update(new TValue(DateTime.UtcNow, 100));
trima.Update(new TValue(DateTime.UtcNow, 105));
double valueBefore = trima.Value;
trima.Reset();
@@ -151,20 +150,24 @@ public class TrimaTests
// Calculate iteratively
var iterativeResults = new TSeries();
#pragma warning disable S4158 // Collection is known to be empty
foreach (var item in series)
{
iterativeResults.Add(trimaIterative.Update(item));
}
#pragma warning restore S4158
// Calculate batch
var batchResults = trimaBatch.Update(series);
// Compare
Assert.Equal(iterativeResults.Count, batchResults.Count);
#pragma warning disable S2583 // Condition always evaluates to false
for (int i = 0; i < iterativeResults.Count; i++)
{
Assert.Equal(iterativeResults[i].Value, batchResults[i].Value, 1e-10);
}
#pragma warning restore S2583
}
[Fact]
+20 -106
View File
@@ -8,29 +8,17 @@ namespace QuanTAlib;
/// TRIMA: Triangular Moving Average
/// </summary>
/// <remarks>
/// TRIMA is a weighted moving average where the weights increase linearly to the middle
/// of the period and then decrease linearly. It places the most weight on the middle
/// portion of the data series.
/// TRIMA is a weighted moving average where weights increase linearly to the middle
/// and then decrease. It is equivalent to a double SMA: SMA(SMA(period1), period2).
///
/// Calculation:
/// TRIMA(period) = SMA(SMA(period1), period2)
/// where:
/// period1 = period / 2 + 1
/// period2 = (period + 1) / 2
///
/// This implementation uses a flattened structure with two internal SMA buffers
/// to ensure correct handling of warmup periods and bar corrections without
/// the overhead of composed objects.
///
/// Key characteristics:
/// - Smoother than SMA
/// - Double smoothing (lag is higher than SMA)
/// - Weights form a triangle
/// Characteristics:
/// - Smoother than SMA, higher lag
/// - O(1) time complexity
/// - O(period) space complexity
///
/// Sources:
/// - https://www.investopedia.com/terms/t/triangularaverage.asp
/// </remarks>
[SkipLocalsInit]
public sealed class Trima
@@ -41,36 +29,22 @@ public sealed class Trima
private readonly RingBuffer _buffer1;
private readonly RingBuffer _buffer2;
// SMA1 State
private double _sum1;
private double _p_sum1;
private double _p_lastInput1;
private double _lastValidValue1;
private double _p_lastValidValue1;
private double _sum1, _p_sum1, _p_lastInput1, _lastValidValue1, _p_lastValidValue1;
private int _tickCount1;
// SMA2 State
private double _sum2;
private double _p_sum2;
private double _p_lastInput2;
private double _sum2, _p_sum2, _p_lastInput2;
private int _tickCount2;
private int _sampleCount;
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 => _sampleCount >= _period;
/// <summary>
/// Creates TRIMA with specified period.
/// </summary>
/// <param name="period">Number of values to average (must be > 0)</param>
public Trima(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;
_p1 = period / 2 + 1;
@@ -82,19 +56,6 @@ public sealed class Trima
Name = $"Trima({period})";
}
/// <summary>
/// Current TRIMA value.
/// </summary>
public TValue Value { get; private set; }
/// <summary>
/// True if the TRIMA has enough data to produce valid results.
/// </summary>
public bool IsHot => _sampleCount >= _period;
/// <summary>
/// Gets a valid input value, using last-value substitution for non-finite inputs.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidValue(double input)
{
@@ -106,12 +67,6 @@ public sealed class Trima
return _lastValidValue1;
}
/// <summary>
/// Updates TRIMA with the given value.
/// </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 TRIMA value</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
{
@@ -119,13 +74,12 @@ public sealed class Trima
{
_sampleCount++;
// SMA 1 Update
// SMA 1
double val1 = GetValidValue(input.Value);
double removed1 = _buffer1.Count == _buffer1.Capacity ? _buffer1.Oldest : 0.0;
_sum1 = _sum1 - removed1 + val1;
_buffer1.Add(val1);
// Resync SMA1
_tickCount1++;
if (_buffer1.IsFull && _tickCount1 >= ResyncInterval)
{
@@ -133,21 +87,17 @@ public sealed class Trima
_sum1 = _buffer1.Sum();
}
// Save SMA1 state
_p_sum1 = _sum1;
_p_lastInput1 = val1;
_p_lastValidValue1 = _lastValidValue1;
// SMA 1 Result
double sma1Result = _sum1 / _buffer1.Count;
// SMA 2 Update (Input is sma1Result)
// Note: sma1Result is always finite if input stream has at least one finite value
// SMA 2
double removed2 = _buffer2.Count == _buffer2.Capacity ? _buffer2.Oldest : 0.0;
_sum2 = _sum2 - removed2 + sma1Result;
_buffer2.Add(sma1Result);
// Resync SMA2
_tickCount2++;
if (_buffer2.IsFull && _tickCount2 >= ResyncInterval)
{
@@ -155,13 +105,10 @@ public sealed class Trima
_sum2 = _buffer2.Sum();
}
// Save SMA2 state
_p_sum2 = _sum2;
_p_lastInput2 = sma1Result;
// Final Result
double trimaResult = _sum2 / _buffer2.Count;
Value = new TValue(input.Time, trimaResult);
Value = new TValue(input.Time, _sum2 / _buffer2.Count);
}
else
{
@@ -177,23 +124,16 @@ public sealed class Trima
_sum2 = _p_sum2 - _p_lastInput2 + sma1Result;
_buffer2.UpdateNewest(sma1Result);
double trimaResult = _sum2 / _buffer2.Count;
Value = new TValue(input.Time, trimaResult);
Value = new TValue(input.Time, _sum2 / _buffer2.Count);
}
return Value;
}
/// <summary>
/// Updates TRIMA with the entire series.
/// </summary>
/// <param name="source">Input series</param>
/// <returns>TRIMA series</returns>
public TSeries Update(TSeries source)
{
if (source.Count == 0) return new TSeries(new List<long>(), new List<double>());
// Use the static Calculate method for performance
int len = source.Count;
var t = new List<long>(len);
var v = new List<double>(len);
@@ -202,43 +142,30 @@ public sealed class Trima
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
var sourceValues = source.Values;
var sourceTimes = source.Times;
Calculate(source.Values, vSpan, _period);
source.Times.CopyTo(tSpan);
Calculate(sourceValues, vSpan, _period);
sourceTimes.CopyTo(tSpan);
// Restore state by replaying the last part
// We need to replay enough to fill both SMAs
// Restore state
int lookback = _p1 + _p2;
int startIndex = Math.Max(0, len - lookback);
// Reset internal state
Reset();
// Replay
for (int i = startIndex; i < len; i++)
{
Update(new TValue(sourceTimes[i], sourceValues[i]), isNew: true);
Update(new TValue(source.Times[i], source.Values[i]), isNew: true);
}
Value = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v);
}
/// <summary>
/// Calculates TRIMA for the entire series using a new instance.
/// </summary>
public static TSeries Calculate(TSeries source, int period)
{
var trima = new Trima(period);
return trima.Update(source);
}
/// <summary>
/// Calculates TRIMA in-place.
/// Uses ArrayPool to allocate temporary buffer and chains optimized SMA calculations.
/// </summary>
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (source.Length != output.Length)
@@ -249,16 +176,12 @@ public sealed class Trima
int p1 = period / 2 + 1;
int p2 = (period + 1) / 2;
// Rent a temporary buffer for the intermediate SMA
double[] tempArray = ArrayPool<double>.Shared.Rent(source.Length);
Span<double> tempSpan = tempArray.AsSpan(0, source.Length);
try
{
// SMA 1
Sma.Calculate(source, tempSpan, p1);
// SMA 2 (TRIMA)
Sma.Calculate(tempSpan, output, p2);
}
finally
@@ -267,24 +190,15 @@ public sealed class Trima
}
}
/// <summary>
/// Resets the TRIMA state.
/// </summary>
public void Reset()
{
_buffer1.Clear();
_buffer2.Clear();
_sum1 = 0;
_p_sum1 = 0;
_p_lastInput1 = 0;
_lastValidValue1 = 0;
_p_lastValidValue1 = 0;
_sum1 = _p_sum1 = _p_lastInput1 = _lastValidValue1 = _p_lastValidValue1 = 0;
_tickCount1 = 0;
_sum2 = 0;
_p_sum2 = 0;
_p_lastInput2 = 0;
_sum2 = _p_sum2 = _p_lastInput2 = 0;
_tickCount2 = 0;
_sampleCount = 0;
-363
View File
@@ -1,363 +0,0 @@
namespace QuanTAlib.Tests;
public class TrimaVectorTests
{
[Fact]
public void Initialization_WithPeriods_Works()
{
int[] periods = { 5, 10, 20 };
var trimaVector = new TrimaVector(periods);
var res = trimaVector.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 TrimaVector(periods));
}
[Fact]
public void Initialization_WithNegativePeriod_ThrowsArgumentException()
{
int[] periods = { 10, -5, 20 };
Assert.Throws<ArgumentOutOfRangeException>(() => new TrimaVector(periods));
}
[Fact]
public void Calc_Streaming_MatchesSingleTrima()
{
int[] periods = { 5, 10, 20 };
var trimaVector = new TrimaVector(periods);
var trimaSingles = periods.Select(p => new Trima(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 = trimaVector.Update(tVal);
for (int i = 0; i < periods.Length; i++)
{
var singleRes = trimaSingles[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_MatchesSingleTrima()
{
int[] periods = { 5, 10, 20 };
var trimaVector = new TrimaVector(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 = trimaVector.Calculate(series);
// Reset and recalculate for comparison
var trimaSingles = periods.Select(p => new Trima(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 = trimaSingles[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 trimaVectorBatch = new TrimaVector(periods);
var trimaVectorStream = new TrimaVector(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 = trimaVectorBatch.Calculate(series);
for (int i = 0; i < len; i++)
{
var tVal = new TValue(new DateTime(t[i], DateTimeKind.Utc), v[i]);
var streamRes = trimaVectorStream.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 instanceTrima = new TrimaVector(periods);
var instanceRes = instanceTrima.Calculate(series);
var staticRes = TrimaVector.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 trimaVector = new TrimaVector(periods);
trimaVector.Update(new TValue(DateTime.UtcNow, 100.0));
trimaVector.Update(new TValue(DateTime.UtcNow, 200.0));
trimaVector.Reset();
var res = trimaVector.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 trimaVector = new TrimaVector(periods);
trimaVector.Update(new TValue(DateTime.UtcNow, 100.0));
trimaVector.Update(new TValue(DateTime.UtcNow, 110.0));
var resultAfterNaN = trimaVector.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 trimaVector = new TrimaVector(periods);
trimaVector.Update(new TValue(DateTime.UtcNow, 100.0));
trimaVector.Update(new TValue(DateTime.UtcNow, 110.0));
var resultAfterPosInf = trimaVector.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
foreach (var result in resultAfterPosInf)
{
Assert.True(double.IsFinite(result.Value));
}
var resultAfterNegInf = trimaVector.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 trimaVector = new TrimaVector(periods);
trimaVector.Update(new TValue(DateTime.UtcNow, 100.0));
trimaVector.Update(new TValue(DateTime.UtcNow, 110.0));
trimaVector.Update(new TValue(DateTime.UtcNow, 120.0));
var r1 = trimaVector.Update(new TValue(DateTime.UtcNow, double.NaN));
var r2 = trimaVector.Update(new TValue(DateTime.UtcNow, double.NaN));
var r3 = trimaVector.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 trimaVector = new TrimaVector(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 = trimaVector.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 trimaVector = new TrimaVector(periods);
trimaVector.Update(new TValue(DateTime.UtcNow, 100.0));
trimaVector.Update(new TValue(DateTime.UtcNow, double.NaN));
trimaVector.Reset();
var result = trimaVector.Update(new TValue(DateTime.UtcNow, 50.0));
Assert.Equal(50.0, result[0].Value, 1e-9);
}
[Fact]
public void NaN_Handling_MatchesSingleTrima()
{
int[] periods = { 5, 10, 20 };
var trimaVector = new TrimaVector(periods);
var trimaSingles = periods.Select(p => new Trima(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 = trimaVector.Update(tVal);
for (int i = 0; i < periods.Length; i++)
{
var singleRes = trimaSingles[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 trimaVector = new TrimaVector(periods);
var result = trimaVector.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.Equal(result[0].Value, trimaVector.Values[0].Value);
Assert.Equal(result[1].Value, trimaVector.Values[1].Value);
}
[Fact]
public void Values_Property_UpdatesAfterCalculate()
{
int[] periods = { 5, 10 };
var trimaVector = new TrimaVector(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 = trimaVector.Calculate(series);
Assert.Equal(results[0].Last.Value, trimaVector.Values[0].Value, 1e-9);
Assert.Equal(results[1].Last.Value, trimaVector.Values[1].Value, 1e-9);
}
[Fact]
public void Update_BarCorrection_WorksCorrectly()
{
int[] periods = { 3 };
var trimaVector = new TrimaVector(periods);
// TRIMA(3) = SMA(SMA(3, 2), 2)
// p1 = 3/2 + 1 = 2
// p2 = (3+1)/2 = 2
// SMA1(2): 10 -> 10
// SMA2(2): 10 -> 10
trimaVector.Update(new TValue(DateTime.UtcNow, 10.0), isNew: true);
// SMA1(2): 10, 20 -> 15
// SMA2(2): 10, 15 -> 12.5
trimaVector.Update(new TValue(DateTime.UtcNow, 20.0), isNew: true);
// SMA1(2): 20, 30 -> 25
// SMA2(2): 15, 25 -> 20
trimaVector.Update(new TValue(DateTime.UtcNow, 30.0), isNew: true);
var res1 = trimaVector.Values[0].Value;
Assert.Equal(20.0, res1, 1e-9);
// Correct the last bar: 30 -> 60
// SMA1(2): 20, 60 -> 40
// SMA2(2): 15, 40 -> 27.5
var res2 = trimaVector.Update(new TValue(DateTime.UtcNow, 60.0), isNew: false);
Assert.Equal(27.5, res2[0].Value, 1e-9);
}
}
-241
View File
@@ -1,241 +0,0 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// Multi-Period Triangular Moving Average (TRIMA) - SIMD optimized.
/// Calculates multiple TRIMAs with different periods for the same input series in parallel.
/// Uses last-value substitution for invalid inputs (NaN/Infinity).
/// </summary>
[SkipLocalsInit]
public class TrimaVector
{
private readonly SmaVector _sma1;
private readonly int _count;
private readonly TValue[] _values;
// Internal state for second stage
private readonly RingBuffer[] _buffers2;
private readonly RingBuffer[] _p_buffers2;
private readonly double[] _lastValidValues2;
/// <summary>
/// Current TRIMA values for all periods.
/// </summary>
public ReadOnlySpan<TValue> Values => _values;
/// <summary>
/// Initializes TrimaVector with specified periods.
/// </summary>
/// <param name="periods">Array of periods (each must be > 0)</param>
public TrimaVector(int[] periods)
{
_count = periods.Length;
_values = new TValue[_count];
_buffers2 = new RingBuffer[_count];
_p_buffers2 = new RingBuffer[_count];
_lastValidValues2 = new double[_count];
int[] p1 = new int[_count];
for (int i = 0; i < _count; i++)
{
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(periods[i], 0);
p1[i] = periods[i] / 2 + 1;
int p2 = (periods[i] + 1) / 2;
_buffers2[i] = new RingBuffer(p2);
_p_buffers2[i] = new RingBuffer(p2);
}
_sma1 = new SmaVector(p1);
}
/// <summary>
/// Resets all TRIMA states.
/// </summary>
public void Reset()
{
_sma1.Reset();
for (int i = 0; i < _count; i++)
{
_buffers2[i].Clear();
_p_buffers2[i].Clear();
}
Array.Clear(_lastValidValues2);
Array.Clear(_values);
}
/// <summary>
/// Updates TRIMAs 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.
/// </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 TRIMA values</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue[] Update(TValue input, bool isNew = true)
{
// First pass: SMA1
var sma1Results = _sma1.Update(input, isNew);
// Second pass: SMA2 (TRIMA)
// We need to feed each SMA1 result into the corresponding SMA2
// Since SmaVector.Update takes a single input, we can't use it directly for vector-to-vector
// However, SmaVector is designed for single input -> multiple periods
// Here we have multiple inputs (from SMA1) -> multiple periods (for SMA2)
// This means we need to update each SMA2 individually, but SmaVector doesn't support that directly
// Wait, SmaVector structure is: one input -> N periods.
// Here we have N inputs (one for each period from SMA1) -> N periods (one for each period in SMA2).
// So we can't use a single SmaVector for the second stage if the inputs are different.
// We need N separate SMAs for the second stage, OR we need to modify SmaVector to support vector input.
// But wait, TrimaVector is supposed to be optimized.
// Let's look at how we can implement this efficiently.
// Actually, since each period in TRIMA maps to a specific pair of (p1, p2),
// and the input to the second SMA depends on the output of the first SMA,
// the inputs to the second stage are indeed all different.
// So we can't use SmaVector for the second stage in the same way (single input broadcast to all).
// We have two options:
// 1. Use an array of Sma objects for the second stage.
// 2. Implement a custom vector-input SMA logic here.
// Given the goal of high performance and vectorization, option 2 is better but more complex.
// However, for now, to match the structure and ensure correctness, let's use the fact that
// we already have SmaVector which is optimized for ring buffers.
// But SmaVector assumes a single input value for all buffers.
// Here, _sma1 produces an array of values, one for each period.
// _sma2 needs to take these DIFFERENT values.
// So, we cannot use SmaVector for the second stage if it only supports single input.
// Let's check SmaVector again. Yes, Update takes `TValue input`.
// So we need to implement the second stage manually using RingBuffers, similar to SmaVector
// but accepting a vector of inputs.
// Let's refactor:
// Instead of using _sma2 as SmaVector, we'll manage the second stage buffers directly here.
// This duplicates some logic from SmaVector but allows vector-to-vector processing.
// Actually, since we are implementing TrimaVector, maybe we should just use arrays of RingBuffers
// for both stages directly, to avoid the mismatch.
// But _sma1 is fine because it takes the single external input.
// It's only the second stage that is problematic.
// Let's implement the second stage buffers directly.
// Wait, I can't change the class structure mid-method.
// I will implement the class using _sma1 for the first stage, and manual buffers for the second stage.
// Re-reading my own thought process:
// _sma1.Update(input) returns TValue[] with results for each period.
// We need to feed result[i] into buffer2[i].
return UpdateInternal(sma1Results, isNew);
}
private TValue[] UpdateInternal(TValue[] inputs, bool isNew)
{
if (isNew)
{
for (int i = 0; i < _count; i++)
{
_p_buffers2[i].CopyFrom(_buffers2[i]);
}
}
else
{
for (int i = 0; i < _count; i++)
{
_buffers2[i].CopyFrom(_p_buffers2[i]);
}
}
for (int i = 0; i < _count; i++)
{
double val = inputs[i].Value;
// Last-value substitution for the second stage
if (double.IsFinite(val))
{
_lastValidValues2[i] = val;
}
else
{
val = _lastValidValues2[i];
}
_buffers2[i].Add(val);
_values[i] = new TValue(inputs[i].Time, _buffers2[i].Average);
}
return _values;
}
/// <summary>
/// Calculates TRIMAs for the entire series.
/// </summary>
/// <param name="source">Input series</param>
/// <returns>Array of TRIMA series</returns>
public TSeries[] Calculate(TSeries source)
{
// We can use the Update method for simplicity and correctness,
// or implement a batch calculation for performance.
// Given the complexity of double smoothing, using Update in a loop is safer and cleaner.
// SmaVector.Calculate is optimized, but we have the two-stage issue.
// Let's use the Update loop approach for now to ensure correctness.
// It will be reasonably fast.
int len = source.Count;
var resultSeries = new TSeries[_count];
// 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);
}
Reset();
for (int t = 0; t < len; t++)
{
var tVal = new TValue(source.Times[t], source.Values[t]);
var results = Update(tVal, isNew: true);
for (int i = 0; i < _count; i++)
{
CollectionsMarshal.AsSpan(tLists[i])[t] = results[i].Time;
CollectionsMarshal.AsSpan(vLists[i])[t] = results[i].Value;
}
}
for (int i = 0; i < _count; i++)
{
resultSeries[i] = new TSeries(tLists[i], vLists[i]);
}
return resultSeries;
}
/// <summary>
/// Calculates TRIMAs for the entire series using specified periods.
/// </summary>
/// <param name="source">Input series</param>
/// <param name="periods">Array of periods</param>
/// <returns>Array of TRIMA series</returns>
public static TSeries[] Calculate(TSeries source, int[] periods)
{
var trimaVector = new TrimaVector(periods);
return trimaVector.Calculate(source);
}
}
+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);
}
}
+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)]
+148
View File
@@ -0,0 +1,148 @@
using Xunit;
using TradingPlatform.BusinessLayer;
using System.Drawing;
using System.Reflection;
namespace QuanTAlib.Tests;
public class IndicatorExtensionsTests
{
private class TestIndicator : Indicator
{
public TestIndicator()
{
Name = "Test Indicator";
}
}
private class TestCoordinatesConverter : ICoordinatesConverter
{
private readonly DateTime _time;
public TestCoordinatesConverter(DateTime time) => _time = time;
public DateTime GetTime(int x) => _time;
public double GetChartX(DateTime time) => 0;
public double GetChartY(double value) => 0;
}
[Fact]
public void DataSourceInputAttribute_HasCorrectDefaults()
{
var attr = new IndicatorExtensions.DataSourceInputAttribute();
Assert.Equal("Data source", attr.Name);
Assert.Equal(20, attr.SortIndex);
Assert.NotNull(attr.Variants);
Assert.NotEmpty(attr.Variants);
}
[Fact]
public void GetInputValue_ReturnsCorrectValues_ForSourceTypes()
{
var indicator = new TestIndicator();
var now = DateTime.UtcNow;
// Open=100, High=110, Low=90, Close=105, Volume=1000
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105, 1000);
// Ensure Count is updated (mock implementation detail)
// The mock HistoricalData.Count reflects added items.
// Indicator.Count => HistoricalData.Count.
var args = new UpdateArgs(UpdateReason.NewBar);
// Test each SourceType
Assert.Equal(100, IndicatorExtensions.GetInputValue(indicator, args, SourceType.Open).Value);
Assert.Equal(110, IndicatorExtensions.GetInputValue(indicator, args, SourceType.High).Value);
Assert.Equal(90, IndicatorExtensions.GetInputValue(indicator, args, SourceType.Low).Value);
Assert.Equal(105, IndicatorExtensions.GetInputValue(indicator, args, SourceType.Close).Value);
// HL2 = (110 + 90) / 2 = 100
Assert.Equal(100, IndicatorExtensions.GetInputValue(indicator, args, SourceType.HL2).Value);
// OC2 = (100 + 105) / 2 = 102.5
Assert.Equal(102.5, IndicatorExtensions.GetInputValue(indicator, args, SourceType.OC2).Value);
// OHL3 = (100 + 110 + 90) / 3 = 100
Assert.Equal(100, IndicatorExtensions.GetInputValue(indicator, args, SourceType.OHL3).Value);
// HLC3 = (110 + 90 + 105) / 3 = 101.666...
Assert.Equal(101.66666666666667, IndicatorExtensions.GetInputValue(indicator, args, SourceType.HLC3).Value, 5);
// OHLC4 = (100 + 110 + 90 + 105) / 4 = 101.25
Assert.Equal(101.25, IndicatorExtensions.GetInputValue(indicator, args, SourceType.OHLC4).Value);
// HLCC4 = (110 + 90 + 105 + 105) / 4 = 102.5
Assert.Equal(102.5, IndicatorExtensions.GetInputValue(indicator, args, SourceType.HLCC4).Value);
}
[Fact]
public void GetInputBar_ReturnsCorrectBar()
{
var indicator = new TestIndicator();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105, 1000);
var args = new UpdateArgs(UpdateReason.NewBar);
var bar = IndicatorExtensions.GetInputBar(indicator, args);
Assert.Equal(now, bar.AsDateTime);
Assert.Equal(100, bar.Open);
Assert.Equal(110, bar.High);
Assert.Equal(90, bar.Low);
Assert.Equal(105, bar.Close);
Assert.Equal(1000, bar.Volume);
}
[Fact]
public void PaintMethods_DoNotThrow_WithValidGraphics()
{
// This test attempts to verify that paint methods don't crash.
// It requires System.Drawing.Common to be functional.
if (!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
{
// Skip on non-Windows if System.Drawing is not fully supported (GDI+)
return;
}
using var bitmap = new Bitmap(100, 100);
using var graphics = Graphics.FromImage(bitmap);
var indicator = new TestIndicator();
indicator.CurrentChart = new MockChart();
// Add some data
var now = new DateTime(2024, 1, 1, 12, 0, 0, DateTimeKind.Utc);
for (int i = 0; i < 20; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 110, 90, 105);
}
// Setup converter to return a time that exists in our data (e.g. the middle bar)
// We added bars at now, now+1min, ..., now+19min.
// Let's return now+10min.
var validTime = now.AddMinutes(10);
indicator.CurrentChart.MainWindow.CoordinatesConverter = new TestCoordinatesConverter(validTime);
var args = new PaintChartEventArgs(graphics, new Rectangle(0, 0, 100, 100));
using var pen = new Pen(Color.Red);
// Test PaintHLine
IndicatorExtensions.PaintHLine(indicator, args, 100, pen);
// Test PaintSmoothCurve
var series = new LineSeries("Test", Color.Blue, 1, LineStyle.Solid);
for (int i = 0; i < 20; i++) series.AddValue(); // Fill with NaNs or values
for (int i = 0; i < 20; i++) series.SetValue(100 + i, i); // Set some values
IndicatorExtensions.PaintSmoothCurve(indicator, args, series, 0);
// Test PaintHistogram
IndicatorExtensions.PaintHistogram(indicator, args, series, 0);
// Test DrawText
IndicatorExtensions.DrawText(indicator, args, "Test Text");
}
}
+143
View File
@@ -0,0 +1,143 @@
using Xunit;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
public class TrimaIndicatorTests
{
[Fact]
public void TrimaIndicator_Constructor_SetsDefaults()
{
var indicator = new TrimaIndicator();
Assert.Equal(10, indicator.Period);
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Equal("TRIMA - Triangular Moving Average", indicator.Name);
Assert.False(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void TrimaIndicator_MinHistoryDepths_EqualsPeriod()
{
var indicator = new TrimaIndicator { Period = 20 };
Assert.Equal(20, indicator.MinHistoryDepths);
Assert.Equal(20, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void TrimaIndicator_ShortName_IncludesPeriodAndSource()
{
var indicator = new TrimaIndicator { Period = 15 };
Assert.Contains("TRIMA", indicator.ShortName);
Assert.Contains("15", indicator.ShortName);
}
[Fact]
public void TrimaIndicator_SourceCodeLink_IsValid()
{
var indicator = new TrimaIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink);
Assert.Contains("Trima.Quantower.cs", indicator.SourceCodeLink);
}
[Fact]
public void TrimaIndicator_Initialize_CreatesInternalTrima()
{
var indicator = new TrimaIndicator { Period = 10 };
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void TrimaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new TrimaIndicator { Period = 3 };
indicator.Initialize();
// Add historical data
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
// Process update
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
// Line series should have a value
Assert.Equal(1, indicator.LinesSeries[0].Count);
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
}
[Fact]
public void TrimaIndicator_MultipleUpdates_ProducesCorrectTrimaSequence()
{
var indicator = new TrimaIndicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
double[] closes = { 100, 102, 104, 103, 105 };
foreach (var close in closes)
{
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
now = now.AddMinutes(1);
}
// All values should be finite
for (int i = 0; i < closes.Length; i++)
{
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
}
// TRIMA is smoothed, so check last value is reasonable
double lastTrima = indicator.LinesSeries[0].GetValue(0);
Assert.True(lastTrima >= 100 && lastTrima <= 106);
}
[Fact]
public void TrimaIndicator_DifferentSourceTypes_Work()
{
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
foreach (var source in sources)
{
var indicator = new TrimaIndicator { Period = 3, Source = source };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
$"Source {source} should produce finite value");
}
}
[Fact]
public void TrimaIndicator_Period_CanBeChanged()
{
var indicator = new TrimaIndicator { Period = 5 };
Assert.Equal(5, indicator.Period);
indicator.Period = 20;
Assert.Equal(20, indicator.Period);
Assert.Equal(20, indicator.MinHistoryDepths);
}
[Fact]
public void TrimaIndicator_DescriptionIsSet()
{
var indicator = new TrimaIndicator();
Assert.Contains("Triangular", indicator.Description);
}
}