Refactor code formatting and improve consistency across various test files

- Removed unnecessary blank lines in multiple test files to enhance readability.
- Ensured consistent spacing and formatting in the `Trima`, `Usf`, `Vidya`, `Wma`, and `Atr` test classes.
- Updated comments for clarity and consistency in the `Atr` and `Adl` classes.
- Adjusted project files for better structure and maintainability.
This commit is contained in:
Miha Kralj
2025-12-28 17:44:08 -08:00
parent ad6eebf812
commit 13d7c1215d
169 changed files with 10815 additions and 10814 deletions
+1 -1
View File
@@ -56,7 +56,7 @@ public class AlmaIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
TValue result = ma!.Update(new TValue(item.TimeLeft.Ticks, _priceSelector!(item)), isNew: args.IsNewBar());
Series!.SetValue(result.Value, ma.IsHot, ShowColdValues);
+3 -3
View File
@@ -255,10 +255,10 @@ public class AlmaTests
{
var alma = new Alma(10);
alma.Update(new TValue(DateTime.UtcNow, 100));
var r1 = alma.Update(new TValue(DateTime.UtcNow, double.NaN));
var r2 = alma.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsFinite(r1.Value));
Assert.True(double.IsFinite(r2.Value));
}
@@ -271,7 +271,7 @@ public class AlmaTests
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// 1. Batch Mode
var batchSeries = Alma.Batch(series, period);
double expected = batchSeries.Last.Value;
+5 -5
View File
@@ -247,8 +247,8 @@ public sealed class Alma : AbstractBase, IDisposable
// Precompute weights
// Use stackalloc for small periods to avoid heap allocation, ArrayPool for large
double[]? weightsArray = period > 256 ? ArrayPool<double>.Shared.Rent(period) : null;
Span<double> weights = period <= 256
? stackalloc double[period]
Span<double> weights = period <= 256
? stackalloc double[period]
: weightsArray!.AsSpan(0, period);
double m = offset * (period - 1);
@@ -266,8 +266,8 @@ public sealed class Alma : AbstractBase, IDisposable
// Buffer for sliding window
double[]? bufferArray = period > 256 ? ArrayPool<double>.Shared.Rent(period) : null;
Span<double> buffer = period <= 256
? stackalloc double[period]
Span<double> buffer = period <= 256
? stackalloc double[period]
: bufferArray!.AsSpan(0, period);
int bufferIdx = 0;
@@ -288,7 +288,7 @@ public sealed class Alma : AbstractBase, IDisposable
// Add to circular buffer
buffer[bufferIdx] = val;
bufferIdx = (bufferIdx + 1) % period;
if (count < period)
{
count++;
+1 -1
View File
@@ -49,7 +49,7 @@ public class BesselIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
TValue result = _filter!.Update(new TValue(item.TimeLeft.Ticks, _priceSelector!(item)), isNew: args.IsNewBar());
Series!.SetValue(result.Value, _filter.IsHot, ShowColdValues);
+1 -1
View File
@@ -12,7 +12,7 @@ public class BesselTests
var bessel = new Bessel(2);
Assert.NotNull(bessel);
var bessel14 = new Bessel(14);
Assert.NotNull(bessel14);
}
+1 -1
View File
@@ -9,7 +9,7 @@ namespace QuanTAlib;
/// <remarks>
/// Bessel filter is a 2nd-order IIR low-pass filter with maximally flat group delay,
/// adapted from John Ehlers' work for financial time series.
///
///
/// Coefficients for a given length L:
/// a = exp(-PI / L)
/// b = 2 * a * cos(1.738 * PI / L)
@@ -166,7 +166,7 @@ public class BilateralIndicatorTests
indicator.Period = 20;
indicator.SigmaSRatio = 1.0;
indicator.SigmaRMult = 2.0;
Assert.Equal(20, indicator.Period);
Assert.Equal(1.0, indicator.SigmaSRatio);
Assert.Equal(2.0, indicator.SigmaRMult);
+1 -1
View File
@@ -56,7 +56,7 @@ public class BilateralIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
TValue result = _bilateral!.Update(new TValue(item.TimeLeft.Ticks, _priceSelector!(item)), isNew: args.IsNewBar());
Series!.SetValue(result.Value, _bilateral.IsHot, ShowColdValues);
+21 -21
View File
@@ -23,13 +23,13 @@ public class BilateralTests
public void IsHot_BecomesTrueWhenBufferFull()
{
var indicator = new Bilateral(3);
indicator.Update(new TValue(DateTime.UtcNow, 1));
Assert.False(indicator.IsHot);
indicator.Update(new TValue(DateTime.UtcNow, 2));
Assert.False(indicator.IsHot);
indicator.Update(new TValue(DateTime.UtcNow, 3));
Assert.True(indicator.IsHot);
}
@@ -42,13 +42,13 @@ public class BilateralTests
// If sigma_r is high, range weights are ~1.
// If sigma_s is high, spatial weights are ~1.
// Then it becomes a simple average.
var indicator = new Bilateral(3, sigmaSRatio: 100, sigmaRMult: 100);
indicator.Update(new TValue(DateTime.UtcNow, 1));
indicator.Update(new TValue(DateTime.UtcNow, 2));
var result = indicator.Update(new TValue(DateTime.UtcNow, 3));
// Expected: (1+2+3)/3 = 2
Assert.Equal(2.0, result.Value, 1);
}
@@ -57,11 +57,11 @@ public class BilateralTests
public void Update_HandlesNaN()
{
var indicator = new Bilateral(3);
indicator.Update(new TValue(DateTime.UtcNow, 1));
indicator.Update(new TValue(DateTime.UtcNow, double.NaN)); // Should use 1
var result = indicator.Update(new TValue(DateTime.UtcNow, 3));
// Buffer: [1, 1, 3]
// StDev of [1, 1, 3]: Mean=1.66, Var=((1-1.66)^2 + (1-1.66)^2 + (3-1.66)^2)/3 = (0.44 + 0.44 + 1.77)/3 = 0.88. StDev ~ 0.94
// Calculation will proceed with these values.
@@ -73,23 +73,23 @@ public class BilateralTests
public void Update_IsNew_False_UpdatesCorrectly()
{
var indicator = new Bilateral(3);
indicator.Update(new TValue(DateTime.UtcNow, 1));
indicator.Update(new TValue(DateTime.UtcNow, 2));
// Update with 3, isNew=true
indicator.Update(new TValue(DateTime.UtcNow, 3), isNew: true);
// Update with 4, isNew=false (correction)
var res2 = indicator.Update(new TValue(DateTime.UtcNow, 4), isNew: false);
// Verify state was updated
// If we had updated with 4 directly: [1, 2, 4]
var indicator2 = new Bilateral(3);
indicator2.Update(new TValue(DateTime.UtcNow, 1));
indicator2.Update(new TValue(DateTime.UtcNow, 2));
var resExpected = indicator2.Update(new TValue(DateTime.UtcNow, 4));
Assert.Equal(resExpected.Value, res2.Value);
}
@@ -100,9 +100,9 @@ public class BilateralTests
indicator.Update(new TValue(DateTime.UtcNow, 1));
indicator.Update(new TValue(DateTime.UtcNow, 2));
indicator.Update(new TValue(DateTime.UtcNow, 3));
indicator.Reset();
Assert.False(indicator.IsHot);
Assert.Equal(1, indicator.Update(new TValue(DateTime.UtcNow, 1)).Value); // Center val 1, weights 0? No, center val is returned if weights 0.
}
@@ -112,10 +112,10 @@ public class BilateralTests
{
// Test edge case: calling Update with isNew:false before any isNew:true
var indicator = new Bilateral(3);
// This should not crash - buffer is empty, so we treat it as first value
var result = indicator.Update(new TValue(DateTime.UtcNow, 5.0), isNew: false);
// Should have added the value to the buffer
Assert.True(double.IsFinite(result.Value));
Assert.Equal(5.0, result.Value); // Single value, so result is that value
@@ -126,18 +126,18 @@ public class BilateralTests
{
// Test edge case: calling Update with isNew:false after Reset
var indicator = new Bilateral(3);
indicator.Update(new TValue(DateTime.UtcNow, 1));
indicator.Update(new TValue(DateTime.UtcNow, 2));
indicator.Reset();
// Buffer is now empty, isNew:false should not crash
var result = indicator.Update(new TValue(DateTime.UtcNow, 7.0), isNew: false);
Assert.True(double.IsFinite(result.Value));
Assert.Equal(7.0, result.Value);
}
[Fact]
public void AllModes_ProduceSameResult()
{
@@ -115,12 +115,12 @@ public sealed class BilateralValidationTests : IDisposable
{
var reference = new BilateralReference(period, sigmaSRatio, sigmaRMult);
var results = new List<double>();
foreach (var item in _testData.Data)
{
results.Add(reference.Update(item.Value));
}
return results;
}
@@ -149,7 +149,7 @@ public sealed class BilateralValidationTests : IDisposable
if (_history.Count == 0) return double.NaN;
double sigmaS = Math.Max(_length * _sigmaSRatio, 1e-10);
// Calculate StDev of current window
double stdev = CalculateStDev(_history);
double sigmaR = Math.Max(stdev * _sigmaRMult, 1e-10);
@@ -161,18 +161,18 @@ public sealed class BilateralValidationTests : IDisposable
// Iterate through history
// i=0 is newest (index Count-1)
int loopLen = _history.Count;
for (int i = 0; i < loopLen; i++)
{
double valI = _history[_history.Count - 1 - i];
double diffSpatial = i;
double diffRange = centerVal - valI;
double weightSpatial = Math.Exp(-(diffSpatial * diffSpatial) / (2.0 * sigmaS * sigmaS));
double weightRange = Math.Exp(-(diffRange * diffRange) / (2.0 * sigmaR * sigmaR));
double weight = weightSpatial * weightRange;
sumWeights += weight;
sumWeightedSrc += weight * valI;
}
@@ -183,7 +183,7 @@ public sealed class BilateralValidationTests : IDisposable
private static double CalculateStDev(IReadOnlyList<double> values)
{
if (values.Count < 2) return 0;
double avg = values.Average();
double sumSqDiff = values.Sum(d => (d - avg) * (d - avg));
// Population StDev to match implementation
+22 -22
View File
@@ -57,7 +57,7 @@ public sealed class Bilateral : AbstractBase
PrecalculateSpatialWeights();
}
public Bilateral(ITValuePublisher source, int period, double sigmaSRatio = 0.5, double sigmaRMult = 1.0)
public Bilateral(ITValuePublisher source, int period, double sigmaSRatio = 0.5, double sigmaRMult = 1.0)
: this(period, sigmaSRatio, sigmaRMult)
{
source.Pub += _handler;
@@ -139,7 +139,7 @@ public sealed class Bilateral : AbstractBase
Update(new TValue(source.Times[i], source.Values[i]));
vSpan[i] = Last.Value;
}
return new TSeries(t, v);
}
@@ -149,10 +149,10 @@ public sealed class Bilateral : AbstractBase
if (isNew)
{
_p_state = _state;
double val = GetValidValue(input.Value);
double removed = _buffer.Add(val);
_state.SumSq += (val * val);
if (_buffer.IsFull)
{
@@ -163,12 +163,12 @@ public sealed class Bilateral : AbstractBase
{
// Preserve SumSq as it tracks the buffer which is already at T
double currentSumSq = _state.SumSq;
_state = _p_state;
_state.SumSq = currentSumSq;
double val = GetValidValue(input.Value);
// Defensive check: if buffer is empty, treat as first value
if (_buffer.Count == 0)
{
@@ -179,7 +179,7 @@ public sealed class Bilateral : AbstractBase
{
double oldNewest = _buffer.Newest; // Get current newest before overwriting
_buffer.UpdateNewest(val);
_state.SumSq -= (oldNewest * oldNewest);
_state.SumSq += (val * val);
}
@@ -210,7 +210,7 @@ public sealed class Bilateral : AbstractBase
// Calculate StDev
double count = _buffer.Count;
double sum = _buffer.Sum;
// Variance = (SumSq - (Sum*Sum)/N) / N
// Use Math.Max(0, ...) to handle potential floating point negative zero
double variance = Math.Max(0, (_state.SumSq - (sum * sum) / count) / count);
@@ -226,12 +226,12 @@ public sealed class Bilateral : AbstractBase
// Iterate from 0 to Count-1
// i=0 corresponds to Newest (src[0])
// i corresponds to buffer[Count - 1 - i]
// Use InternalBuffer to avoid allocations from GetSpan() when wrapped
ReadOnlySpan<double> buffer = _buffer.InternalBuffer;
int capacity = _buffer.Capacity;
int startIndex = _buffer.StartIndex;
// Newest element index
int newestIndex = (startIndex + (int)count - 1) % capacity;
@@ -241,16 +241,16 @@ public sealed class Bilateral : AbstractBase
// (newestIndex - i) handling wrap-around
int idx = newestIndex - i;
if (idx < 0) idx += capacity;
double val = buffer[idx];
double diffRange = centerVal - val;
// weight_spatial = _spatialWeights[i]
// weight_range = exp(-(diff^2) / (2 * sigma_r^2))
double weightRange = Math.Exp(-(diffRange * diffRange) / twoSigmaRSq);
double weight = _spatialWeights[i] * weightRange;
sumWeights += weight;
sumWeightedSrc += weight * val;
}
@@ -284,7 +284,7 @@ public sealed class Bilateral : AbstractBase
if (destination.Length < source.Length)
throw new ArgumentException("Destination must have length >= source length", nameof(destination));
// Precalculate spatial weights
double sigmaS = Math.Max(period * sigmaSRatio, 1e-10);
double twoSigmaSSq = 2.0 * sigmaS * sigmaS;
@@ -306,7 +306,7 @@ public sealed class Bilateral : AbstractBase
break;
}
}
// If all NaNs, fill with NaN
if (double.IsNaN(lastValid))
{
@@ -340,11 +340,11 @@ public sealed class Bilateral : AbstractBase
sum -= removed;
sumSq -= removed * removed;
}
window[windowIdx] = val;
sum += val;
sumSq += val * val;
int currentNewestIdx = windowIdx;
windowIdx = (windowIdx + 1) % period;
if (count < period) count++;
@@ -367,13 +367,13 @@ public sealed class Bilateral : AbstractBase
// k=1 is previous...
int idx = currentNewestIdx - k;
if (idx < 0) idx += period;
double wVal = window[idx];
double diffRange = centerVal - wVal;
double weightRange = Math.Exp(-(diffRange * diffRange) / twoSigmaRSq);
double weight = spatialWeights[k] * weightRange;
sumWeights += weight;
sumWeightedSrc += weight * wVal;
}
+1 -1
View File
@@ -50,7 +50,7 @@ public class BlmaIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
TValue result = _ma!.Update(new TValue(item.TimeLeft.Ticks, _priceSelector!(item)), isNew: args.IsNewBar());
_series!.SetValue(result.Value, _ma.IsHot, ShowColdValues);
+28 -28
View File
@@ -33,15 +33,15 @@ public class BlmaTests
{
var blma = new Blma(3);
var input = new[] { 10.0, 20.0, 30.0 };
// Bar 1: Count=1. Weights for n=1: [1]. Result = 10.
var r1 = blma.Update(new TValue(DateTime.UtcNow, input[0]));
Assert.Equal(10.0, r1.Value);
// Bar 2: Count=2. Weights for n=2 sum to 0. Fallback to average: (10+20)/2 = 15.
var r2 = blma.Update(new TValue(DateTime.UtcNow, input[1]));
Assert.Equal(15.0, r2.Value);
// Bar 3: Count=3. Weights [0, 1, 0]. Sum=1. Result=20.
var r3 = blma.Update(new TValue(DateTime.UtcNow, input[2]));
Assert.Equal(20.0, r3.Value, 1e-6);
@@ -82,45 +82,45 @@ public class BlmaTests
public void NaN_Handling()
{
var blma = new Blma(5);
blma.Update(new TValue(DateTime.UtcNow, 10));
blma.Update(new TValue(DateTime.UtcNow, 20));
// For N=2, weights sum to 0. Fallback to average: (10+20)/2 = 15.
var result = blma.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.Equal(15.0, result.Value); // Should return last valid value
Assert.Equal(15.0, blma.Last.Value); // Should retain last valid value
}
[Fact]
public void IsNew_Behavior()
{
var blma = new Blma(3);
// Bar 1
blma.Update(new TValue(DateTime.UtcNow, 10), isNew: true);
// Bar 2
blma.Update(new TValue(DateTime.UtcNow, 20), isNew: true);
// Bar 3 (Update)
blma.Update(new TValue(DateTime.UtcNow, 30), isNew: true);
var val1 = blma.Last.Value;
// Bar 3 (Correction)
blma.Update(new TValue(DateTime.UtcNow, 40), isNew: false);
var val2 = blma.Last.Value;
// For Blackman window, the newest value (index N-1) has weight 0.
// So changing the newest value does NOT change the current result.
Assert.Equal(val1, val2);
// However, the internal buffer MUST be updated.
// Case A: Bar 3 = 40 (current state)
blma.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
var valWith40 = blma.Last.Value;
// Case B: Reconstruct scenario with Bar 3 = 30
var blma2 = new Blma(3);
blma2.Update(new TValue(DateTime.UtcNow, 10), isNew: true);
@@ -128,7 +128,7 @@ public class BlmaTests
blma2.Update(new TValue(DateTime.UtcNow, 30), isNew: true);
blma2.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
var valWith30 = blma2.Last.Value;
Assert.NotEqual(valWith30, valWith40);
}
@@ -138,12 +138,12 @@ public class BlmaTests
var blma = new Blma(5);
var input = new double[] { 1, 2, 3, 4, 5 };
var timestamps = new List<DateTime>();
blma.Pub += (object? sender, TValueEventArgs args) => timestamps.Add(args.Value.AsDateTime);
blma.Pub += (object? sender, in TValueEventArgs args) => timestamps.Add(args.Value.AsDateTime);
blma.Prime(input);
Assert.Equal(input.Length, timestamps.Count);
// Verify timestamps are unique and increasing
for (int i = 1; i < timestamps.Count; i++)
@@ -157,19 +157,19 @@ public class BlmaTests
{
var blma = new Blma(5);
var now = DateTime.UtcNow;
TValue[] input =
[
new(now, 1),
new(now.AddMinutes(1), 2),
new(now.AddMinutes(2), 3)
TValue[] input =
[
new(now, 1),
new(now.AddMinutes(1), 2),
new(now.AddMinutes(2), 3)
];
var timestamps = new List<DateTime>();
blma.Pub += (object? sender, TValueEventArgs args) => timestamps.Add(args.Value.AsDateTime);
blma.Pub += (object? sender, in TValueEventArgs args) => timestamps.Add(args.Value.AsDateTime);
blma.Prime(input);
Assert.Equal(input.Length, timestamps.Count);
Assert.Equal(input[0].AsDateTime, timestamps[0]);
Assert.Equal(input[1].AsDateTime, timestamps[1]);
+8 -8
View File
@@ -50,15 +50,15 @@ public class BlmaValidationTests
for (int i = 0; i < source.Count; i++)
{
buffer.Add(source[i].Value);
// PineScript logic:
// int p = math.min(bar_index + 1, period)
int p = Math.Min(buffer.Count, period);
// Calculate weights
var weights = new double[p];
double totalWeight = 0;
if (p == 1)
{
weights[0] = 1.0;
@@ -88,15 +88,15 @@ public class BlmaValidationTests
// float price = source[i] (where source[0] is newest)
// float w = array.get(weights, i)
// So weights[0] * newest, weights[1] * 2nd newest...
// My C# buffer is chronological (0 is oldest).
// So buffer[buffer.Count - 1] is newest.
// buffer[buffer.Count - 1 - j] is j-th lag.
// Wait, in Blma.cs I implemented:
// sum += buffer[i] * weights[i] (where buffer[0] is oldest)
// So weights[0] * oldest.
// PineScript: weights[0] * newest.
// Since Blackman window is symmetric, weights[0] == weights[p-1].
// So weights[0] * newest == weights[p-1] * newest (if symmetric).
@@ -111,11 +111,11 @@ public class BlmaValidationTests
// cos(4pi * (1-r)) = cos(4pi - 4pi*r) = cos(4pi*r).
// So yes, w(j) == w(p-1-j).
// So applying weights[0] to newest or oldest doesn't matter for the sum.
// However, I should match my implementation in Blma.cs.
// In Blma.cs: sum += buffer[i] * weights[i] (buffer[0] is oldest).
// So weights[0] * oldest.
// In this reference implementation, let's do the same.
// Use the last p elements of buffer.
int start = buffer.Count - p;
+6 -6
View File
@@ -29,7 +29,7 @@ public sealed class Blma : AbstractBase, IDisposable
WarmupPeriod = period;
_buffer = new RingBuffer(period);
_weights = new double[period];
// Pre-calculate weights for the full period
_weightSum = CalculateWeights(period, _weights);
_handler = Handle;
@@ -102,7 +102,7 @@ public sealed class Blma : AbstractBase, IDisposable
{
Span<double> currentWeights = stackalloc double[count];
double currentWeightSum = CalculateWeights(count, currentWeights);
// Fallback for cases where weights sum to zero (e.g. N=2)
result = Math.Abs(currentWeightSum) < double.Epsilon
? _buffer.Average()
@@ -130,7 +130,7 @@ public sealed class Blma : AbstractBase, IDisposable
var result = new TSeries();
Span<double> output = new double[source.Count];
Calculate(source.Values, output, _period);
for (int i = 0; i < source.Count; i++)
{
result.Add(new TValue(source[i].Time, output[i]));
@@ -182,7 +182,7 @@ public sealed class Blma : AbstractBase, IDisposable
int start = buffer.StartIndex;
int count = buffer.Count;
int capacity = buffer.Capacity;
if (start + count <= capacity)
{
return buffer.InternalBuffer.Slice(start, count).DotProduct(weights);
@@ -219,7 +219,7 @@ public sealed class Blma : AbstractBase, IDisposable
for (int i = 0; i < source.Length; i++)
{
int count = Math.Min(i + 1, period);
if (count < period)
{
// Warmup: dynamic weights
@@ -231,7 +231,7 @@ public sealed class Blma : AbstractBase, IDisposable
{
Span<double> currentWeights = warmupWeightsBuffer.Slice(0, count);
double currentWeightSum = CalculateWeights(count, currentWeights);
if (Math.Abs(currentWeightSum) < double.Epsilon)
{
// Fallback for zero sum weights (e.g. N=2)
+1 -1
View File
@@ -50,7 +50,7 @@ public class ButterIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
TValue result = _ma!.Update(new TValue(item.TimeLeft.Ticks, _priceSelector!(item)), isNew: args.IsNewBar());
_series!.SetValue(result.Value, _ma.IsHot, ShowColdValues);
+7 -7
View File
@@ -45,7 +45,7 @@ public class ButterTests
butter.Update(new TValue(DateTime.UtcNow, 100));
butter.Update(new TValue(DateTime.UtcNow, 101));
Assert.True(butter.IsHot);
butter.Reset();
Assert.False(butter.IsHot);
}
@@ -107,30 +107,30 @@ public class ButterTests
Assert.Equal(expected, streamingResult, 1e-9);
Assert.Equal(expected, eventingResult, 1e-9);
}
[Fact]
public void IterativeCorrections_RestoreToOriginalState()
{
int period = 10;
var butter = new Butter(period);
// Feed 10 values
for (int i = 0; i < 10; i++)
{
butter.Update(new TValue(DateTime.UtcNow, 100 + i));
}
double expected = butter.Last.Value;
// Feed 5 updates with isNew=false
for (int i = 0; i < 5; i++)
{
butter.Update(new TValue(DateTime.UtcNow, 200 + i), isNew: false);
}
// Feed original 10th value again with isNew=false
var result = butter.Update(new TValue(DateTime.UtcNow, 109), isNew: false);
Assert.Equal(expected, result.Value, 1e-9);
}
}
+17 -17
View File
@@ -78,7 +78,7 @@ public class ButterValidationTests
// Compare
Assert.Equal(quantalibResult.Count, ooplesValues.Count);
// Check last 100 bars
for (int i = quantalibResult.Count - 100; i < quantalibResult.Count; i++)
{
@@ -91,7 +91,7 @@ public class ButterValidationTests
private static IReadOnlyList<double> CalculateReference(TSeries source, int period)
{
var result = new List<double>();
// PineScript logic:
// float pi = math.pi
// int safe_length = math.max(length, 2)
@@ -105,7 +105,7 @@ public class ButterValidationTests
// float b0 = (1.0 - cos_omega) / 2.0
// float b1 = 1.0 - cos_omega
// float b2 = (1.0 - cos_omega) / 2.0
int safe_length = Math.Max(period, 2);
double omega = 2.0 * Math.PI / safe_length;
double sin_omega = Math.Sin(omega);
@@ -117,22 +117,22 @@ public class ButterValidationTests
double b0 = (1.0 - cos_omega) / 2.0;
double b1 = 1.0 - cos_omega;
double b2 = (1.0 - cos_omega) / 2.0;
double filt = 0;
double filt1 = 0;
double filt2 = 0;
// Need to track history for src[1], src[2]
// In PineScript, src[1] is previous bar's src.
// We iterate through source.
double src1 = 0;
double src2 = 0;
for (int i = 0; i < source.Count; i++)
{
double src = source[i].Value;
// if bar_index < 2
// filt := nz(src, 0.0)
if (i < 2)
@@ -142,38 +142,38 @@ public class ButterValidationTests
// In PineScript, src[1] at index 0 is NaN (nz -> 0.0 or something?)
// Actually, nz(src, 0.0) means if src is NaN, use 0.0.
// But here src is valid.
// At i=0: src[1] is NaN, src[2] is NaN.
// At i=1: src[1] is src[i-1], src[2] is NaN.
// But the PineScript code says:
// if bar_index < 2: filt := nz(src, 0.0)
// else: ... formula ...
// So for i=0 and i=1, filt = src.
}
else
{
// float ssrc = nz(src, src[1]) -> if src is NaN use src[1]. Assuming src is valid.
double ssrc = src;
// float src1 = nz(src[1], ssrc) -> previous src.
// float src2 = nz(src[2], src1) -> 2nd previous src.
// float filt1 = nz(filt[1], ssrc) -> previous filt.
// float filt2 = nz(filt[2], filt1) -> 2nd previous filt.
// filt := (b0 * ssrc + b1 * src1 + b2 * src2 - a1 * filt1 - a2 * filt2) / a0
filt = (b0 * ssrc + b1 * src1 + b2 * src2 - a1 * filt1 - a2 * filt2) / a0;
}
result.Add(filt);
// Update history
src2 = src1;
src1 = src;
filt2 = filt1;
filt1 = filt;
}
+5 -5
View File
@@ -58,7 +58,7 @@ public sealed class Butter : AbstractBase
double a0 = 1.0 + alpha;
a1 = -2.0 * cosOmega;
a2 = 1.0 - alpha;
b0 = (1.0 - cosOmega) / 2.0;
b1 = 1.0 - cosOmega;
b2 = (1.0 - cosOmega) / 2.0;
@@ -123,7 +123,7 @@ public sealed class Butter : AbstractBase
_state.X1 = x;
_state.Y2 = _state.Y1;
_state.Y1 = y;
if (_state.Count < 2)
{
_state.Count++;
@@ -140,7 +140,7 @@ public sealed class Butter : AbstractBase
var result = new TSeries();
Span<double> output = new double[source.Count];
Calculate(source.Values, output, _period, double.NaN);
for (int i = 0; i < source.Count; i++)
{
result.Add(new TValue(source[i].Time, output[i]));
@@ -148,11 +148,11 @@ public sealed class Butter : AbstractBase
// Restore state
Reset();
// Replay a reasonable amount (e.g. 4*period) for convergence of IIR state.
int replayCount = Math.Min(source.Count, 4 * _period);
int start = source.Count - replayCount;
for (int i = start; i < source.Count; i++)
{
Update(source[i]);
+1 -1
View File
@@ -66,7 +66,7 @@ public class ConvIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
TValue result = _conv!.Update(new TValue(item.TimeLeft.Ticks, _priceSelector!(item)), isNew: args.IsNewBar());
Series!.SetValue(result.Value, _conv.IsHot, ShowColdValues);
+1 -1
View File
@@ -171,7 +171,7 @@ public class ConvTests
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// 1. Batch Mode
var batchSeries = Conv.Batch(series, kernel);
double expected = batchSeries.Last.Value;
+1 -1
View File
@@ -50,7 +50,7 @@ public class DemaIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
TValue result = ma!.Update(new TValue(item.TimeLeft.Ticks, _priceSelector!(item)), isNew: args.IsNewBar());
Series!.SetValue(result.Value, ma.IsHot, ShowColdValues);
+8 -8
View File
@@ -22,7 +22,7 @@ public class DemaTests
var tVal = new TValue(bar.Time, bar.Close);
var dVal = dema.Update(tVal);
var e1Val = ema1.Update(tVal);
var e2Val = ema2.Update(e1Val);
double expected = 2 * e1Val.Value - e2Val.Value;
@@ -48,7 +48,7 @@ public class DemaTests
// Act
var demaSeries = Dema.Calculate(source, period);
var demaObj = new Dema(period);
// Assert
for (int i = 0; i < source.Count; i++)
{
@@ -134,7 +134,7 @@ public class DemaTests
// Act
var demaSeries = Dema.Calculate(source, alpha);
var demaObj = new Dema(alpha);
// Assert
for (int i = 0; i < source.Count; i++)
{
@@ -193,9 +193,9 @@ public class DemaTests
var dema = new Dema(10);
dema.Update(new TValue(DateTime.UtcNow, 100));
dema.Update(new TValue(DateTime.UtcNow, 110));
dema.Reset();
Assert.Equal(0, dema.Last.Value);
Assert.False(dema.IsHot);
}
@@ -278,7 +278,7 @@ public class DemaTests
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// 1. Batch Mode
var batchSeries = Dema.Calculate(series, period);
double expected = batchSeries.Last.Value;
@@ -321,11 +321,11 @@ public class DemaTests
Dema.Calculate(source, output, 3);
// We expect the first two outputs to be NaN because the input was NaN
// We expect the first two outputs to be NaN because the input was NaN
Assert.True(double.IsNaN(output[0]), $"Output[0] should be NaN, but was {output[0]}");
Assert.True(double.IsNaN(output[1]), $"Output[1] should be NaN, but was {output[1]}");
// The first valid value is 10.0.
// The first valid value is 10.0.
Assert.Equal(10.0, output[2], 1e-9);
}
}
+4 -4
View File
@@ -106,13 +106,13 @@ public sealed class DemaValidationTests : IDisposable
var demaIndicator = Tulip.Indicators.dema;
double[][] inputs = { tData };
double[] options = { period };
// Tulip DEMA lookback is usually period-1 for EMA, but DEMA is 2*EMA - EMA(EMA)
// Let's rely on the output length to align.
// Tulip DEMA lookback is same as EMA lookback? No, it involves double smoothing.
// Actually, Tulip's DEMA implementation might have a specific lookback.
// We'll calculate it based on output length.
// Tulip.Indicators.dema.Run expects outputs to be sized correctly.
// We'll use a large buffer and resize if needed, or just calculate lookback.
// For DEMA(n), lookback is roughly n-1 (same as EMA).
@@ -120,7 +120,7 @@ public sealed class DemaValidationTests : IDisposable
// Let's try with n-1 first, if it fails we adjust.
// Actually, TA-Lib DEMA lookback is 2*(period-1).
// Let's assume Tulip is similar.
int lookback = 2 * (period - 1);
int lookback = 2 * (period - 1);
double[][] outputs = { new double[tData.Length - lookback] };
demaIndicator.Run(inputs, options, outputs);
@@ -177,7 +177,7 @@ public sealed class DemaValidationTests : IDisposable
for (int i = 0; i < _testData.Data.Count; i++)
{
var item = _testData.Data[i];
// QuanTAlib DEMA
var qVal = dema.Update(item);
+1 -1
View File
@@ -50,7 +50,7 @@ public class DwmaIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
TValue result = ma!.Update(new TValue(item.TimeLeft.Ticks, _priceSelector!(item)), isNew: args.IsNewBar());
Series!.SetValue(result.Value, ma.IsHot, ShowColdValues);
+16 -16
View File
@@ -22,20 +22,20 @@ public class DwmaTests
// 3: (1*1 + 2*2 + 3*3) / 6 = 14/6 = 2.333...
// 4: (1*2 + 2*3 + 3*4) / 6 = 20/6 = 3.333...
// 5: (1*3 + 2*4 + 3*5) / 6 = 26/6 = 4.333...
// WMA(3) results: [1, 1.666, 2.333, 3.333, 4.333]
// DWMA(3) = WMA(3) of [1, 1.666, 2.333, 3.333, 4.333]
// 1: 1
// 2: (1*1 + 2*1.666) / 3 = 4.333/3 = 1.444...
// 3: (1*1 + 2*1.666 + 3*2.333) / 6 = (1 + 3.333 + 7) / 6 = 11.333/6 = 1.888...
var dwma = new Dwma(3);
var v1 = dwma.Update(new TValue(DateTime.UtcNow, 1)).Value;
var v2 = dwma.Update(new TValue(DateTime.UtcNow, 2)).Value;
var v3 = dwma.Update(new TValue(DateTime.UtcNow, 3)).Value;
Assert.Equal(1.0, v1, 6);
Assert.Equal(1.444444, v2, 5);
Assert.Equal(1.888888, v3, 5);
@@ -45,23 +45,23 @@ public class DwmaTests
public void Update_IsNewFalse_CorrectsValue()
{
var dwma = new Dwma(3);
dwma.Update(new TValue(DateTime.UtcNow, 1));
dwma.Update(new TValue(DateTime.UtcNow, 2));
// Update with 3, then correct to 4
var v3 = dwma.Update(new TValue(DateTime.UtcNow, 3), isNew: true).Value;
var v3_corrected = dwma.Update(new TValue(DateTime.UtcNow, 4), isNew: false).Value;
// Manual calc for sequence [1, 2, 4]
// WMA(3):
// 1: 1
// 2: 1.666
// 4: (1*1 + 2*2 + 3*4) / 6 = 17/6 = 2.8333
// DWMA(3) of [1, 1.666, 2.8333]
// 3: (1*1 + 2*1.666 + 3*2.8333) / 6 = (1 + 3.333 + 8.5) / 6 = 12.833/6 = 2.1388
Assert.Equal(1.888888, v3, 5); // From previous test
Assert.Equal(2.138888, v3_corrected, 5);
}
@@ -72,9 +72,9 @@ public class DwmaTests
var dwma = new Dwma(3);
dwma.Update(new TValue(DateTime.UtcNow, 1));
dwma.Update(new TValue(DateTime.UtcNow, 2));
dwma.Reset();
Assert.False(dwma.IsHot);
var v1 = dwma.Update(new TValue(DateTime.UtcNow, 1)).Value;
Assert.Equal(1.0, v1);
@@ -87,15 +87,15 @@ public class DwmaTests
int count = 100;
var source = new TSeries();
var dwma = new Dwma(period);
for (int i = 0; i < count; i++)
{
source.Add(new TValue(DateTime.UtcNow.AddMinutes(i), i));
dwma.Update(source.Last);
}
var staticResult = Dwma.Batch(source, period);
Assert.Equal(source.Count, staticResult.Count);
Assert.Equal(dwma.Last.Value, staticResult.Last.Value, 8);
}
@@ -178,7 +178,7 @@ public class DwmaTests
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// 1. Batch Mode
var batchSeries = Dwma.Batch(series, period);
double expected = batchSeries.Last.Value;
+12 -12
View File
@@ -46,24 +46,24 @@ public sealed class DwmaValidationTests : IDisposable
public void Validate_Against_DoubleWma()
{
// DWMA should be exactly WMA(WMA(source, period), period)
int period = 10;
var dwma = new Dwma(period);
var wma1 = new Wma(period);
var wma2 = new Wma(period);
for (int i = 0; i < _testData.Data.Count; i++)
{
var val = _testData.Data[i];
// Calculate DWMA
var dwmaVal = dwma.Update(val);
// Calculate WMA(WMA) manually
var wma1Val = wma1.Update(val);
var wma2Val = wma2.Update(wma1Val);
Assert.Equal(wma2Val.Value, dwmaVal.Value, ValidationHelper.DefaultTolerance);
}
}
@@ -73,24 +73,24 @@ public sealed class DwmaValidationTests : IDisposable
{
// Ooples Finance does not have a specific DWMA indicator, but it can be calculated
// by chaining two Weighted Moving Averages
int period = 14;
var dwma = new Dwma(period);
var wma1 = new Wma(period); // Simulates first CalculateWeightedMovingAverage
var wma2 = new Wma(period); // Simulates second CalculateWeightedMovingAverage
for (int i = 0; i < _testData.Data.Count; i++)
{
var val = _testData.Data[i];
// QuanTAlib DWMA
var qVal = dwma.Update(val);
// Ooples Logic (Chained WMA)
var w1 = wma1.Update(val);
var w2 = wma2.Update(w1);
Assert.Equal(w2.Value, qVal.Value, ValidationHelper.DefaultTolerance);
}
}
File diff suppressed because it is too large Load Diff
+320 -320
View File
@@ -1,320 +1,320 @@
using System;
using System.Collections.Generic;
using System.Linq;
using OoplesFinance.StockIndicators;
using OoplesFinance.StockIndicators.Models;
using Skender.Stock.Indicators;
using TALib;
using Tulip;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
public sealed class EmaValidationTests : IDisposable
{
private readonly ValidationTestData _testData;
private readonly ITestOutputHelper _output;
private bool _disposed;
public EmaValidationTests(ITestOutputHelper output)
{
_output = output;
_testData = new ValidationTestData();
}
public void Dispose()
{
Dispose(true);
}
private void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{
_testData?.Dispose();
}
}
[Fact]
public void Validate_Skender_Batch()
{
int[] periods = { 5, 10, 20, 50, 100 };
foreach (var period in periods)
{
// Calculate QuanTAlib EMA (batch TSeries)
var ema = new global::QuanTAlib.Ema(period);
var qResult = ema.Update(_testData.Data);
// Calculate Skender EMA
var sResult = _testData.SkenderQuotes.GetEma(period).ToList();
// Compare last 100 records
ValidationHelper.VerifyData(qResult, sResult, (s) => s.Ema);
}
_output.WriteLine("EMA Batch(TSeries) validated successfully against Skender");
}
[Fact]
public void Validate_Skender_Streaming()
{
int[] periods = { 5, 10, 20, 50, 100 };
foreach (var period in periods)
{
// Calculate QuanTAlib EMA (streaming)
var ema = new global::QuanTAlib.Ema(period);
var qResults = new List<double>();
foreach (var item in _testData.Data)
{
qResults.Add(ema.Update(item).Value);
}
// Calculate Skender EMA
var sResult = _testData.SkenderQuotes.GetEma(period).ToList();
// Compare last 100 records
ValidationHelper.VerifyData(qResults, sResult, (s) => s.Ema);
}
_output.WriteLine("EMA Streaming validated successfully against Skender");
}
[Fact]
public void Validate_Skender_Span()
{
int[] periods = { 5, 10, 20, 50, 100 };
// Prepare data for Span API
double[] sourceData = _testData.RawData.ToArray();
foreach (var period in periods)
{
// Calculate QuanTAlib EMA (Span API)
double[] qOutput = new double[sourceData.Length];
global::QuanTAlib.Ema.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period);
// Calculate Skender EMA
var sResult = _testData.SkenderQuotes.GetEma(period).ToList();
// Compare last 100 records
ValidationHelper.VerifyData(qOutput, sResult, (s) => s.Ema);
}
_output.WriteLine("EMA Span validated successfully against Skender");
}
[Fact]
public void Validate_Talib_Batch()
{
int[] periods = { 5, 10, 20, 50, 100 };
// Prepare data for TA-Lib (double[])
double[] tData = _testData.RawData.ToArray();
double[] output = new double[tData.Length];
foreach (var period in periods)
{
// Calculate QuanTAlib EMA (batch TSeries)
var ema = new global::QuanTAlib.Ema(period);
var qResult = ema.Update(_testData.Data);
// Calculate TA-Lib EMA
var retCode = TALib.Functions.Ema<double>(tData, 0..^0, output, out var outRange, period);
Assert.Equal(Core.RetCode.Success, retCode);
int lookback = TALib.Functions.EmaLookback(period);
// Compare last 100 records
ValidationHelper.VerifyData(qResult, output, outRange, lookback);
}
_output.WriteLine("EMA Batch(TSeries) validated successfully against TA-Lib");
}
[Fact]
public void Validate_Talib_Streaming()
{
int[] periods = { 5, 10, 20, 50, 100 };
// Prepare data for TA-Lib (double[])
double[] tData = _testData.RawData.ToArray();
double[] output = new double[tData.Length];
foreach (var period in periods)
{
// Calculate QuanTAlib EMA (streaming)
var ema = new global::QuanTAlib.Ema(period);
var qResults = new List<double>();
foreach (var item in _testData.Data)
{
qResults.Add(ema.Update(item).Value);
}
// Calculate TA-Lib EMA
var retCode = TALib.Functions.Ema<double>(tData, 0..^0, output, out var outRange, period);
Assert.Equal(Core.RetCode.Success, retCode);
int lookback = TALib.Functions.EmaLookback(period);
// Compare last 100 records
ValidationHelper.VerifyData(qResults, output, outRange, lookback);
}
_output.WriteLine("EMA Streaming validated successfully against TA-Lib");
}
[Fact]
public void Validate_Talib_Span()
{
int[] periods = { 5, 10, 20, 50, 100 };
// Prepare data
double[] sourceData = _testData.RawData.ToArray();
double[] talibOutput = new double[sourceData.Length];
foreach (var period in periods)
{
// Calculate QuanTAlib EMA (Span API)
double[] qOutput = new double[sourceData.Length];
global::QuanTAlib.Ema.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period);
// Calculate TA-Lib EMA
var retCode = TALib.Functions.Ema<double>(sourceData, 0..^0, talibOutput, out var outRange, period);
Assert.Equal(Core.RetCode.Success, retCode);
int lookback = TALib.Functions.EmaLookback(period);
// Compare last 100 records
ValidationHelper.VerifyData(qOutput, talibOutput, outRange, lookback);
}
_output.WriteLine("EMA Span validated successfully against TA-Lib");
}
[Fact]
public void Validate_Tulip_Batch()
{
int[] periods = { 5, 10, 20, 50, 100 };
// Prepare data for Tulip (double[])
double[] tData = _testData.RawData.ToArray();
foreach (var period in periods)
{
// Calculate QuanTAlib EMA (batch TSeries)
var ema = new global::QuanTAlib.Ema(period);
var qResult = ema.Update(_testData.Data);
// Calculate Tulip EMA
var emaIndicator = Tulip.Indicators.ema;
double[][] inputs = { tData };
double[] options = { period };
double[][] outputs = { new double[tData.Length] };
emaIndicator.Run(inputs, options, outputs);
var tResult = outputs[0];
// Compare last 100 records
ValidationHelper.VerifyData(qResult, tResult, 0);
}
_output.WriteLine("EMA Batch(TSeries) validated successfully against Tulip");
}
[Fact]
public void Validate_Tulip_Streaming()
{
int[] periods = { 5, 10, 20, 50, 100 };
// Prepare data for Tulip (double[])
double[] tData = _testData.RawData.ToArray();
foreach (var period in periods)
{
// Calculate QuanTAlib EMA (streaming)
var ema = new global::QuanTAlib.Ema(period);
var qResults = new List<double>();
foreach (var item in _testData.Data)
{
qResults.Add(ema.Update(item).Value);
}
// Calculate Tulip EMA
var emaIndicator = Tulip.Indicators.ema;
double[][] inputs = { tData };
double[] options = { period };
double[][] outputs = { new double[tData.Length] };
emaIndicator.Run(inputs, options, outputs);
var tResult = outputs[0];
// Compare last 100 records
ValidationHelper.VerifyData(qResults, tResult, 0);
}
_output.WriteLine("EMA Streaming validated successfully against Tulip");
}
[Fact]
public void Validate_Tulip_Span()
{
int[] periods = { 5, 10, 20, 50, 100 };
// Prepare data
double[] sourceData = _testData.RawData.ToArray();
foreach (var period in periods)
{
// Calculate QuanTAlib EMA (Span API)
double[] qOutput = new double[sourceData.Length];
global::QuanTAlib.Ema.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period);
// Calculate Tulip EMA
var emaIndicator = Tulip.Indicators.ema;
double[][] inputs = { sourceData };
double[] options = { period };
double[][] outputs = { new double[sourceData.Length] };
emaIndicator.Run(inputs, options, outputs);
var tResult = outputs[0];
// Compare last 100 records
ValidationHelper.VerifyData(qOutput, tResult, 0);
}
_output.WriteLine("EMA Span validated successfully against Tulip");
}
[Fact]
public void Validate_Against_Ooples()
{
int[] periods = { 5, 10, 20, 50, 100 };
// Prepare data for Ooples (List<TickerData>)
var ooplesData = _testData.SkenderQuotes.Select(q => new TickerData
{
Date = q.Date,
Close = (double)q.Close,
High = (double)q.High,
Low = (double)q.Low,
Open = (double)q.Open,
Volume = (double)q.Volume
}).ToList();
foreach (var period in periods)
{
// Calculate QuanTAlib EMA
var ema = new global::QuanTAlib.Ema(period);
var qResult = ema.Update(_testData.Data);
// Calculate Ooples EMA
var stockData = new StockData(ooplesData);
var oResult = stockData.CalculateExponentialMovingAverage(period);
var oValues = oResult.OutputValues.Values.First();
// Compare
ValidationHelper.VerifyData(qResult, oValues, (s) => s, tolerance: ValidationHelper.OoplesTolerance);
}
_output.WriteLine("EMA validated successfully against Ooples");
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using OoplesFinance.StockIndicators;
using OoplesFinance.StockIndicators.Models;
using Skender.Stock.Indicators;
using TALib;
using Tulip;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
public sealed class EmaValidationTests : IDisposable
{
private readonly ValidationTestData _testData;
private readonly ITestOutputHelper _output;
private bool _disposed;
public EmaValidationTests(ITestOutputHelper output)
{
_output = output;
_testData = new ValidationTestData();
}
public void Dispose()
{
Dispose(true);
}
private void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{
_testData?.Dispose();
}
}
[Fact]
public void Validate_Skender_Batch()
{
int[] periods = { 5, 10, 20, 50, 100 };
foreach (var period in periods)
{
// Calculate QuanTAlib EMA (batch TSeries)
var ema = new global::QuanTAlib.Ema(period);
var qResult = ema.Update(_testData.Data);
// Calculate Skender EMA
var sResult = _testData.SkenderQuotes.GetEma(period).ToList();
// Compare last 100 records
ValidationHelper.VerifyData(qResult, sResult, (s) => s.Ema);
}
_output.WriteLine("EMA Batch(TSeries) validated successfully against Skender");
}
[Fact]
public void Validate_Skender_Streaming()
{
int[] periods = { 5, 10, 20, 50, 100 };
foreach (var period in periods)
{
// Calculate QuanTAlib EMA (streaming)
var ema = new global::QuanTAlib.Ema(period);
var qResults = new List<double>();
foreach (var item in _testData.Data)
{
qResults.Add(ema.Update(item).Value);
}
// Calculate Skender EMA
var sResult = _testData.SkenderQuotes.GetEma(period).ToList();
// Compare last 100 records
ValidationHelper.VerifyData(qResults, sResult, (s) => s.Ema);
}
_output.WriteLine("EMA Streaming validated successfully against Skender");
}
[Fact]
public void Validate_Skender_Span()
{
int[] periods = { 5, 10, 20, 50, 100 };
// Prepare data for Span API
double[] sourceData = _testData.RawData.ToArray();
foreach (var period in periods)
{
// Calculate QuanTAlib EMA (Span API)
double[] qOutput = new double[sourceData.Length];
global::QuanTAlib.Ema.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period);
// Calculate Skender EMA
var sResult = _testData.SkenderQuotes.GetEma(period).ToList();
// Compare last 100 records
ValidationHelper.VerifyData(qOutput, sResult, (s) => s.Ema);
}
_output.WriteLine("EMA Span validated successfully against Skender");
}
[Fact]
public void Validate_Talib_Batch()
{
int[] periods = { 5, 10, 20, 50, 100 };
// Prepare data for TA-Lib (double[])
double[] tData = _testData.RawData.ToArray();
double[] output = new double[tData.Length];
foreach (var period in periods)
{
// Calculate QuanTAlib EMA (batch TSeries)
var ema = new global::QuanTAlib.Ema(period);
var qResult = ema.Update(_testData.Data);
// Calculate TA-Lib EMA
var retCode = TALib.Functions.Ema<double>(tData, 0..^0, output, out var outRange, period);
Assert.Equal(Core.RetCode.Success, retCode);
int lookback = TALib.Functions.EmaLookback(period);
// Compare last 100 records
ValidationHelper.VerifyData(qResult, output, outRange, lookback);
}
_output.WriteLine("EMA Batch(TSeries) validated successfully against TA-Lib");
}
[Fact]
public void Validate_Talib_Streaming()
{
int[] periods = { 5, 10, 20, 50, 100 };
// Prepare data for TA-Lib (double[])
double[] tData = _testData.RawData.ToArray();
double[] output = new double[tData.Length];
foreach (var period in periods)
{
// Calculate QuanTAlib EMA (streaming)
var ema = new global::QuanTAlib.Ema(period);
var qResults = new List<double>();
foreach (var item in _testData.Data)
{
qResults.Add(ema.Update(item).Value);
}
// Calculate TA-Lib EMA
var retCode = TALib.Functions.Ema<double>(tData, 0..^0, output, out var outRange, period);
Assert.Equal(Core.RetCode.Success, retCode);
int lookback = TALib.Functions.EmaLookback(period);
// Compare last 100 records
ValidationHelper.VerifyData(qResults, output, outRange, lookback);
}
_output.WriteLine("EMA Streaming validated successfully against TA-Lib");
}
[Fact]
public void Validate_Talib_Span()
{
int[] periods = { 5, 10, 20, 50, 100 };
// Prepare data
double[] sourceData = _testData.RawData.ToArray();
double[] talibOutput = new double[sourceData.Length];
foreach (var period in periods)
{
// Calculate QuanTAlib EMA (Span API)
double[] qOutput = new double[sourceData.Length];
global::QuanTAlib.Ema.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period);
// Calculate TA-Lib EMA
var retCode = TALib.Functions.Ema<double>(sourceData, 0..^0, talibOutput, out var outRange, period);
Assert.Equal(Core.RetCode.Success, retCode);
int lookback = TALib.Functions.EmaLookback(period);
// Compare last 100 records
ValidationHelper.VerifyData(qOutput, talibOutput, outRange, lookback);
}
_output.WriteLine("EMA Span validated successfully against TA-Lib");
}
[Fact]
public void Validate_Tulip_Batch()
{
int[] periods = { 5, 10, 20, 50, 100 };
// Prepare data for Tulip (double[])
double[] tData = _testData.RawData.ToArray();
foreach (var period in periods)
{
// Calculate QuanTAlib EMA (batch TSeries)
var ema = new global::QuanTAlib.Ema(period);
var qResult = ema.Update(_testData.Data);
// Calculate Tulip EMA
var emaIndicator = Tulip.Indicators.ema;
double[][] inputs = { tData };
double[] options = { period };
double[][] outputs = { new double[tData.Length] };
emaIndicator.Run(inputs, options, outputs);
var tResult = outputs[0];
// Compare last 100 records
ValidationHelper.VerifyData(qResult, tResult, 0);
}
_output.WriteLine("EMA Batch(TSeries) validated successfully against Tulip");
}
[Fact]
public void Validate_Tulip_Streaming()
{
int[] periods = { 5, 10, 20, 50, 100 };
// Prepare data for Tulip (double[])
double[] tData = _testData.RawData.ToArray();
foreach (var period in periods)
{
// Calculate QuanTAlib EMA (streaming)
var ema = new global::QuanTAlib.Ema(period);
var qResults = new List<double>();
foreach (var item in _testData.Data)
{
qResults.Add(ema.Update(item).Value);
}
// Calculate Tulip EMA
var emaIndicator = Tulip.Indicators.ema;
double[][] inputs = { tData };
double[] options = { period };
double[][] outputs = { new double[tData.Length] };
emaIndicator.Run(inputs, options, outputs);
var tResult = outputs[0];
// Compare last 100 records
ValidationHelper.VerifyData(qResults, tResult, 0);
}
_output.WriteLine("EMA Streaming validated successfully against Tulip");
}
[Fact]
public void Validate_Tulip_Span()
{
int[] periods = { 5, 10, 20, 50, 100 };
// Prepare data
double[] sourceData = _testData.RawData.ToArray();
foreach (var period in periods)
{
// Calculate QuanTAlib EMA (Span API)
double[] qOutput = new double[sourceData.Length];
global::QuanTAlib.Ema.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period);
// Calculate Tulip EMA
var emaIndicator = Tulip.Indicators.ema;
double[][] inputs = { sourceData };
double[] options = { period };
double[][] outputs = { new double[sourceData.Length] };
emaIndicator.Run(inputs, options, outputs);
var tResult = outputs[0];
// Compare last 100 records
ValidationHelper.VerifyData(qOutput, tResult, 0);
}
_output.WriteLine("EMA Span validated successfully against Tulip");
}
[Fact]
public void Validate_Against_Ooples()
{
int[] periods = { 5, 10, 20, 50, 100 };
// Prepare data for Ooples (List<TickerData>)
var ooplesData = _testData.SkenderQuotes.Select(q => new TickerData
{
Date = q.Date,
Close = (double)q.Close,
High = (double)q.High,
Low = (double)q.Low,
Open = (double)q.Open,
Volume = (double)q.Volume
}).ToList();
foreach (var period in periods)
{
// Calculate QuanTAlib EMA
var ema = new global::QuanTAlib.Ema(period);
var qResult = ema.Update(_testData.Data);
// Calculate Ooples EMA
var stockData = new StockData(ooplesData);
var oResult = stockData.CalculateExponentialMovingAverage(period);
var oValues = oResult.OutputValues.Values.First();
// Compare
ValidationHelper.VerifyData(qResult, oValues, (s) => s, tolerance: ValidationHelper.OoplesTolerance);
}
_output.WriteLine("EMA validated successfully against Ooples");
}
}
+2 -2
View File
@@ -22,7 +22,7 @@ namespace QuanTAlib;
/// No buffer required, only previous EMA value and compensator state.
///
/// IsHot:
/// Becomes true when n = ln(0.05) / ln(1 - alpha)
/// Becomes true when n = ln(0.05) / ln(1 - alpha)
/// </remarks>
[SkipLocalsInit]
public sealed class Ema : AbstractBase
@@ -303,7 +303,7 @@ public sealed class Ema : AbstractBase
else
val = lastValidValue;
state.Ema = Math.FusedMultiplyAdd(state.Ema, decay, alpha * val);
state.E *= decay;
+67 -67
View File
@@ -1,67 +1,67 @@
# EMA: Exponential Moving Average
> "The AK-47 of technical indicators. It's been around forever, everyone uses it, and it gets the job done. It's not fancy, but it works."
EMA (Exponential Moving Average) is the standard by which all other averages are judged. Unlike the SMA, which treats data from 10 days ago with the same reverence as data from 10 seconds ago, the EMA understands that in markets, recency is relevance. It applies an exponentially decaying weight to older prices, reacting faster to new information.
## Historical Context
The EMA was brought to the financial world to solve the "drop-off effect" of the SMA (where an old price dropping out of the window causes the average to jump). By using a recursive formula, the EMA includes *all* past data in its calculation, with weights diminishing to infinity. It is the infinite impulse response (IIR) filter of the trading world.
## Architecture & Physics
The EMA is defined by its smoothing factor, $\alpha$.
- **High $\alpha$**: Fast decay, responsive, noisy.
- **Low $\alpha$**: Slow decay, smooth, laggy.
The QuanTAlib implementation includes a **Compensator** for the warmup phase. A standard EMA starts at 0 (or the first price) and takes time to converge. This early-stage bias is corrected mathematically so the EMA is accurate from the very first few bars, rather than waiting for $3 \times N$ bars to stabilize.
## Mathematical Foundation
The standard recursive formula:
$$ \alpha = \frac{2}{N + 1} $$
$$ \text{EMA}_t = \alpha \cdot P_t + (1 - \alpha) \cdot \text{EMA}_{t-1} $$
### The Compensator (Warmup Correction)
To handle the initialization bias (where $\text{EMA}_0$ is unknown), the sum of weights is tracked:
$$ E_t = (1 - \alpha)^t $$
$$ \text{Corrected EMA}_t = \frac{\text{Uncorrected EMA}_t}{1 - E_t} $$
This ensures the EMA is statistically valid even during the warmup period.
## Performance Profile
This is as fast as it gets.
| Metric | Score | Notes |
| :--- | :--- | :--- |
| **Throughput** | ★★★★★ | Single multiplication and addition. |
| **Allocations** | ★★★★★ | 0 bytes; hot path is allocation-free. |
| **Complexity** | ★★★★★ | O(1) recursive calculation. |
| **Precision** | ★★★★★ | `double` precision. |
### Zero-Allocation Design
EMA is implemented using a simple scalar state variable. The calculation is purely algebraic and requires no heap allocations during the `Update` cycle.
## Validation
Validated against TA-Lib, Skender, Tulip, and Ooples.
| Library | Status | Notes |
| :--- | :--- | :--- |
| **TA-Lib** | ✅ | Matches `TA_EMA`. |
| **Skender** | ✅ | Matches `GetEma`. |
| **Tulip** | ✅ | Matches `ema`. |
| **Ooples** | ✅ | Matches `CalculateExponentialMovingAverage`. |
### Common Pitfalls
1. **The "First Value" Problem**: Most libraries seed the EMA with the first price or an SMA of the first N prices. In QuanTAlib, a mathematical compensator is used. Results during the first N bars are *more accurate* than TA-Lib, which might look like a discrepancy. It is not; the QuanTAlib implementation is correct and TA-Lib is approximating.
2. **Alpha vs. Period**: Remember that $N$ is just a proxy for $\alpha$. You can construct an EMA directly with an $\alpha$ (e.g., 0.1) if you prefer signal processing terminology over trader terminology.
# EMA: Exponential Moving Average
> "The AK-47 of technical indicators. It's been around forever, everyone uses it, and it gets the job done. It's not fancy, but it works."
EMA (Exponential Moving Average) is the standard by which all other averages are judged. Unlike the SMA, which treats data from 10 days ago with the same reverence as data from 10 seconds ago, the EMA understands that in markets, recency is relevance. It applies an exponentially decaying weight to older prices, reacting faster to new information.
## Historical Context
The EMA was brought to the financial world to solve the "drop-off effect" of the SMA (where an old price dropping out of the window causes the average to jump). By using a recursive formula, the EMA includes *all* past data in its calculation, with weights diminishing to infinity. It is the infinite impulse response (IIR) filter of the trading world.
## Architecture & Physics
The EMA is defined by its smoothing factor, $\alpha$.
- **High $\alpha$**: Fast decay, responsive, noisy.
- **Low $\alpha$**: Slow decay, smooth, laggy.
The QuanTAlib implementation includes a **Compensator** for the warmup phase. A standard EMA starts at 0 (or the first price) and takes time to converge. This early-stage bias is corrected mathematically so the EMA is accurate from the very first few bars, rather than waiting for $3 \times N$ bars to stabilize.
## Mathematical Foundation
The standard recursive formula:
$$ \alpha = \frac{2}{N + 1} $$
$$ \text{EMA}_t = \alpha \cdot P_t + (1 - \alpha) \cdot \text{EMA}_{t-1} $$
### The Compensator (Warmup Correction)
To handle the initialization bias (where $\text{EMA}_0$ is unknown), the sum of weights is tracked:
$$ E_t = (1 - \alpha)^t $$
$$ \text{Corrected EMA}_t = \frac{\text{Uncorrected EMA}_t}{1 - E_t} $$
This ensures the EMA is statistically valid even during the warmup period.
## Performance Profile
This is as fast as it gets.
| Metric | Score | Notes |
| :--- | :--- | :--- |
| **Throughput** | ★★★★★ | Single multiplication and addition. |
| **Allocations** | ★★★★★ | 0 bytes; hot path is allocation-free. |
| **Complexity** | ★★★★★ | O(1) recursive calculation. |
| **Precision** | ★★★★★ | `double` precision. |
### Zero-Allocation Design
EMA is implemented using a simple scalar state variable. The calculation is purely algebraic and requires no heap allocations during the `Update` cycle.
## Validation
Validated against TA-Lib, Skender, Tulip, and Ooples.
| Library | Status | Notes |
| :--- | :--- | :--- |
| **TA-Lib** | ✅ | Matches `TA_EMA`. |
| **Skender** | ✅ | Matches `GetEma`. |
| **Tulip** | ✅ | Matches `ema`. |
| **Ooples** | ✅ | Matches `CalculateExponentialMovingAverage`. |
### Common Pitfalls
1. **The "First Value" Problem**: Most libraries seed the EMA with the first price or an SMA of the first N prices. In QuanTAlib, a mathematical compensator is used. Results during the first N bars are *more accurate* than TA-Lib, which might look like a discrepancy. It is not; the QuanTAlib implementation is correct and TA-Lib is approximating.
2. **Alpha vs. Period**: Remember that $N$ is just a proxy for $\alpha$. You can construct an EMA directly with an $\alpha$ (e.g., 0.1) if you prefer signal processing terminology over trader terminology.
+1 -1
View File
@@ -50,7 +50,7 @@ public class HmaIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
TValue result = ma!.Update(new TValue(item.TimeLeft.Ticks, _priceSelector!(item)), isNew: args.IsNewBar());
Series!.SetValue(result.Value, ma.IsHot, ShowColdValues);
+4 -4
View File
@@ -27,7 +27,7 @@ public class HmaTests
// Full WMA needs 9
// Half WMA needs 4
// Sqrt WMA needs 3
// Pipeline:
// Pipeline:
// 1. Full/Half produce valid values immediately (but with warmup ramp)
// 2. Sqrt consumes them.
// IsHot is defined as Full.IsHot && Sqrt.IsHot.
@@ -160,9 +160,9 @@ public class HmaTests
var hma = new Hma(10);
hma.Update(new TValue(DateTime.UtcNow, 100));
hma.Update(new TValue(DateTime.UtcNow, 110));
hma.Reset();
Assert.Equal(0, hma.Last.Value);
Assert.False(hma.IsHot);
}
@@ -245,7 +245,7 @@ public class HmaTests
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// 1. Batch Mode
var batchSeries = Hma.Batch(series, period);
double expected = batchSeries.Last.Value;
+7 -7
View File
@@ -35,7 +35,7 @@ public class HtitTests
var series = data;
var resultSeries = htit.Update(series);
// Reset and calculate streaming
htit.Reset();
var streamingResults = new List<double>();
@@ -58,10 +58,10 @@ public class HtitTests
var series = data;
var resultSeries = htit.Update(series);
var spanInput = data.Values.ToArray();
var spanOutput = new double[spanInput.Length];
Htit.Calculate(spanInput, spanOutput);
for (int i = 0; i < resultSeries.Count; i++)
@@ -76,7 +76,7 @@ public class HtitTests
var htit = new Htit();
htit.Update(new TValue(DateTime.UtcNow.Ticks, 100.0));
htit.Update(new TValue(DateTime.UtcNow.Ticks, double.NaN));
Assert.Equal(100.0, htit.Last.Value);
}
@@ -94,9 +94,9 @@ public class HtitTests
var htit = new Htit();
htit.Update(new TValue(DateTime.UtcNow, 100));
htit.Update(new TValue(DateTime.UtcNow, 110));
htit.Reset();
Assert.True(double.IsNaN(htit.Last.Value));
Assert.False(htit.IsHot);
}
@@ -163,7 +163,7 @@ public class HtitTests
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// 1. Batch Mode
var batchSeries = Htit.Batch(series);
double expected = batchSeries.Last.Value;
+1 -1
View File
@@ -106,7 +106,7 @@ public sealed class HtitValidationTests : IDisposable
// Calculate QuanTAlib HTIT Streaming
var htit = new Htit();
var streamingResults = new List<double>();
foreach (var item in _data.Data)
{
streamingResults.Add(htit.Update(item).Value);
+1 -1
View File
@@ -310,7 +310,7 @@ public sealed class Htit : AbstractBase
for (int i = 0; i < source.Length; i++)
{
double price = source[i];
// Handle non-finite input: skip processing if no valid price seen yet
if (!double.IsFinite(price))
{
+1 -1
View File
@@ -61,7 +61,7 @@ public class JmaIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
TValue result = ma!.Update(new TValue(item.TimeLeft.Ticks, _priceSelector!(item)), isNew: args.IsNewBar());
Series!.SetValue(result.Value, ma.IsHot, ShowColdValues);
+1 -1
View File
@@ -183,7 +183,7 @@ public class JmaTests
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// 1. Batch Mode
var batchSeries = Jma.Batch(series, period);
double expected = batchSeries.Last.Value;
+5 -5
View File
@@ -10,10 +10,10 @@ public class JmaValidationTests
{
// JMA should generally follow the price.
// If price goes up, JMA should eventually go up.
var jma = new Jma(10);
double previousJma = 0;
// Uptrend
for (int i = 0; i < 100; i++)
{
@@ -32,15 +32,15 @@ public class JmaValidationTests
// JMA should stay within the range of recent prices (roughly)
// It's a moving average, so it shouldn't overshoot wildly unless phase is negative and high volatility?
// With default phase 0, it should be well behaved.
var jma = new Jma(10);
var gbm = new GBM(startPrice: 100, mu: 0, sigma: 0.5);
for (int i = 0; i < 1000; i++)
{
var bar = gbm.Next(isNew: true);
var result = jma.Update(new TValue(bar.Time, bar.Close));
if (i > 20)
{
// Update bounds of recent price history (simplified)
+2 -2
View File
@@ -31,11 +31,11 @@ public class JmaZeroDivTests
// Since we can't easily access private fields, we'll rely on the calculation logic check
// If the fix is applied, we shouldn't see -Infinity in internal calculations if we could see them.
// But we can check if the output is exactly the input, which implies adapt=0 (if logic holds).
var jma = new Jma(period: 1);
var result = jma.Update(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, result.Value);
result = jma.Update(new TValue(DateTime.UtcNow, 200));
// If adapt is 0 (due to -Infinity log), bands snap to price.
// If JMA(1) is identity, result should be 200.
+3 -3
View File
@@ -157,7 +157,7 @@ public class KamaTests
Assert.Equal(0, kama.Last.Value);
Assert.False(kama.IsHot);
}
[Fact]
public void Kama_FlatLine_ReturnsSameValue()
{
@@ -166,7 +166,7 @@ public class KamaTests
{
kama.Update(new TValue(DateTime.UtcNow, 100));
}
Assert.Equal(100, kama.Last.Value);
}
@@ -243,7 +243,7 @@ public class KamaTests
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// 1. Batch Mode
var batchSeries = Kama.Batch(series, period);
double expected = batchSeries.Last.Value;
+2 -2
View File
@@ -191,7 +191,7 @@ public sealed class KamaValidationTests : IDisposable
var kamaIndicator = Tulip.Indicators.kama;
double[][] inputs = { cData };
double[] options = { period };
// Tulip KAMA lookback
int lookback = kamaIndicator.Start(options);
double[][] outputs = { new double[cData.Length - lookback] };
@@ -227,7 +227,7 @@ public sealed class KamaValidationTests : IDisposable
var kamaIndicator = Tulip.Indicators.kama;
double[][] inputs = { cData };
double[] options = { period };
// Tulip KAMA lookback
int lookback = kamaIndicator.Start(options);
double[][] outputs = { new double[cData.Length - lookback] };
@@ -28,7 +28,7 @@ Configuration for AI behavior when interacting with Codacy's MCP Server
- Wait for the user to respond before proceeding with any other actions
## After every response
- If you made any file edits in this conversation, verify you ran `codacy_cli_analyze` tool from Codacy's MCP Server
- If you made any file edits in this conversation, verify you ran `codacy_cli_analyze` tool from Codacy's MCP Server
## When there are no Codacy MCP Server tools available, or the MCP Server is not reachable
- Suggest the user the following troubleshooting steps:
+37 -37
View File
@@ -36,7 +36,7 @@ public class LsmaTests
// For a perfect linear trend y = x, LSMA should return x
int period = 10;
var lsma = new Lsma(period);
for (int i = 0; i < period * 2; i++)
{
var result = lsma.Update(new TValue(DateTime.UtcNow, i));
@@ -53,7 +53,7 @@ public class LsmaTests
int period = 10;
var lsma = new Lsma(period);
double value = 123.45;
for (int i = 0; i < period * 2; i++)
{
var result = lsma.Update(new TValue(DateTime.UtcNow, value));
@@ -67,16 +67,16 @@ public class LsmaTests
// y = 2x + 1
// At x=10, y=21. Slope=2, Intercept=1
// LSMA(offset=1) should project to x=11 -> y=23
int period = 5;
int offset = 1;
var lsma = new Lsma(period, offset);
for (int i = 0; i < 20; i++)
{
double y = 2 * i + 1;
var result = lsma.Update(new TValue(DateTime.UtcNow, y));
if (i >= period)
{
double expected = 2 * (i + offset) + 1;
@@ -89,21 +89,21 @@ public class LsmaTests
public void Update_BarCorrection_UpdatesCorrectly()
{
var lsma = new Lsma(5);
// Fill buffer
for (int i = 0; i < 5; i++)
{
lsma.Update(new TValue(DateTime.UtcNow, i));
}
// New bar
var result1 = lsma.Update(new TValue(DateTime.UtcNow, 10));
// Update same bar with different value
var result2 = lsma.Update(new TValue(DateTime.UtcNow, 20), isNew: false);
Assert.NotEqual(result1.Value, result2.Value);
// Verify internal state by adding next bar
// If state was corrupted, this would fail
var result3 = lsma.Update(new TValue(DateTime.UtcNow, 30));
@@ -114,11 +114,11 @@ public class LsmaTests
public void Update_NaN_HandlesGracefully()
{
var lsma = new Lsma(5);
lsma.Update(new TValue(DateTime.UtcNow, 1));
lsma.Update(new TValue(DateTime.UtcNow, 2));
var result = lsma.Update(new TValue(DateTime.UtcNow, double.NaN));
// Input sequence becomes: 1, 2, 2 (NaN replaced by last valid 2)
// Regression on (2,1), (1,2), (0,2)
// Result should be 2.166666667
@@ -132,17 +132,17 @@ public class LsmaTests
int count = 100;
var source = new TSeries();
var gbm = new GBM(startPrice: 100, seed: 42);
for (int i = 0; i < count; i++)
{
var bar = gbm.Next();
source.Add(bar.C);
}
var lsma = new Lsma(period);
var series1 = lsma.Update(source);
var series2 = Lsma.Batch(source, period);
Assert.Equal(series1.Count, series2.Count);
for (int i = 0; i < count; i++)
{
@@ -158,15 +158,15 @@ public class LsmaTests
var values = new double[count];
var output = new double[count];
var gbm = new GBM(startPrice: 100, seed: 42);
for (int i = 0; i < count; i++)
{
var bar = gbm.Next();
values[i] = bar.Close;
}
Lsma.Calculate(values, output, period);
var lsma = new Lsma(period);
for (int i = 0; i < count; i++)
{
@@ -183,14 +183,14 @@ public class LsmaTests
{
lsma.Update(new TValue(DateTime.UtcNow, i));
}
Assert.True(lsma.IsHot);
lsma.Reset();
Assert.False(lsma.IsHot);
Assert.Equal(0, lsma.Last.Value);
// Should behave like new instance
var result = lsma.Update(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, result.Value);
@@ -201,13 +201,13 @@ public class LsmaTests
{
int period = 5;
var lsma = new Lsma(period);
for (int i = 0; i < period; i++)
{
Assert.False(lsma.IsHot);
lsma.Update(new TValue(DateTime.UtcNow, i));
}
Assert.True(lsma.IsHot);
}
@@ -216,7 +216,7 @@ public class LsmaTests
{
var source = new TSeries();
var lsma = new Lsma(source, 10);
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, lsma.Last.Value);
}
@@ -226,14 +226,14 @@ public class LsmaTests
{
var source = new TSeries();
var lsma = new Lsma(source, 5);
// Verify subscription works
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, lsma.Last.Value);
// Dispose and verify unsubscription
lsma.Dispose();
// Add more data - lsma should NOT update
source.Add(new TValue(DateTime.UtcNow, 200));
Assert.Equal(100, lsma.Last.Value); // Should remain at previous value
@@ -244,9 +244,9 @@ public class LsmaTests
{
var source = new TSeries();
var lsma = new Lsma(source, 5);
source.Add(new TValue(DateTime.UtcNow, 100));
// Multiple Dispose calls should not throw
// Suppressing S3966: Multiple Dispose calls are intentional to test idempotency
#pragma warning disable S3966
@@ -254,7 +254,7 @@ public class LsmaTests
lsma.Dispose();
lsma.Dispose();
#pragma warning restore S3966
// Verify still unsubscribed
source.Add(new TValue(DateTime.UtcNow, 200));
Assert.Equal(100, lsma.Last.Value);
@@ -265,18 +265,18 @@ public class LsmaTests
{
var source = new TSeries();
var lsma = new Lsma(source, 5);
source.Add(new TValue(DateTime.UtcNow, 100));
// Dispose from multiple threads simultaneously
var tasks = new System.Threading.Tasks.Task[10];
for (int i = 0; i < tasks.Length; i++)
{
tasks[i] = System.Threading.Tasks.Task.Run(() => lsma.Dispose());
}
await System.Threading.Tasks.Task.WhenAll(tasks);
// Verify unsubscribed
source.Add(new TValue(DateTime.UtcNow, 200));
Assert.Equal(100, lsma.Last.Value);
@@ -287,14 +287,14 @@ public class LsmaTests
{
// Lsma created without source parameter
var lsma = new Lsma(5);
// Should not throw even though there's no source to unsubscribe from
// Suppressing S3966: Multiple Dispose calls are intentional to test idempotency
#pragma warning disable S3966
lsma.Dispose();
lsma.Dispose(); // Idempotent
#pragma warning restore S3966
// Verify state remains valid
Assert.False(lsma.IsHot);
}
+27 -27
View File
@@ -41,21 +41,21 @@ public class MamaTests
public void Update_InfinityInputs_DoesNotHang()
{
var mama = new Mama();
// Warmup with valid data to get past initialization phase
for (int i = 0; i < 60; i++)
{
mama.Update(new TValue(DateTime.UtcNow, 100.0 + i));
}
// Test positive infinity - should not hang
var result1 = mama.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
Assert.True(double.IsFinite(result1.Value), "Positive infinity should produce finite result");
// Test negative infinity - should not hang
var result2 = mama.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
Assert.True(double.IsFinite(result2.Value), "Negative infinity should produce finite result");
// Test NaN - should not hang
var result3 = mama.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsFinite(result3.Value), "NaN should produce finite result");
@@ -66,25 +66,25 @@ public class MamaTests
{
var data = new double[100];
var gbm = new GBM(startPrice: 100, seed: 42);
// Fill with mostly valid data
for (int i = 0; i < 100; i++)
{
data[i] = gbm.Next().Close;
}
// Insert non-finite values at various points
data[20] = double.NaN;
data[40] = double.PositiveInfinity;
data[60] = double.NegativeInfinity;
data[80] = double.NaN;
var output = new double[100];
var famaOutput = new double[100];
// This should complete without hanging
Mama.Calculate(data, output, famaOutput: famaOutput);
// Verify all outputs are finite (no NaN or Infinity propagation)
for (int i = 0; i < 100; i++)
{
@@ -113,7 +113,7 @@ public class MamaTests
// Manually chain for test
bool eventFired = false;
mama.Pub += (object? sender, TValueEventArgs args) => eventFired = true;
mama.Pub += (object? sender, in TValueEventArgs args) => eventFired = true;
mama.Update(new TValue(DateTime.UtcNow, 100.0));
@@ -160,14 +160,14 @@ public class MamaTests
public void IsHot_BecomesTrueAfterWarmup()
{
var mama = new Mama();
// MAMA needs 50 bars to warmup (Index > 50)
for (int i = 0; i < 50; i++)
{
mama.Update(new TValue(DateTime.UtcNow, 100));
Assert.False(mama.IsHot);
}
mama.Update(new TValue(DateTime.UtcNow, 100));
Assert.True(mama.IsHot);
}
@@ -181,9 +181,9 @@ public class MamaTests
mama.Update(new TValue(DateTime.UtcNow, 100));
}
Assert.True(mama.IsHot);
mama.Reset();
Assert.False(mama.IsHot);
Assert.True(double.IsNaN(mama.Last.Value));
}
@@ -192,21 +192,21 @@ public class MamaTests
public void Update_BarCorrection_UpdatesCorrectly()
{
var mama = new Mama();
// Warmup
for (int i = 0; i < 10; i++)
{
mama.Update(new TValue(DateTime.UtcNow, 100));
}
// New bar
var result1 = mama.Update(new TValue(DateTime.UtcNow, 110));
// Update same bar with different value
var result2 = mama.Update(new TValue(DateTime.UtcNow, 120), isNew: false);
Assert.NotEqual(result1.Value, result2.Value);
// Verify internal state by adding next bar
var result3 = mama.Update(new TValue(DateTime.UtcNow, 130));
Assert.True(double.IsFinite(result3.Value));
@@ -217,17 +217,17 @@ public class MamaTests
{
var source = new TSeries();
var gbm = new GBM(startPrice: 100, seed: 42);
for (int i = 0; i < 50; i++)
{
var bar = gbm.Next();
source.Add(bar.C);
}
var mama = new Mama();
var series1 = mama.Update(source);
var series2 = Mama.Batch(source);
Assert.Equal(series1.Count, series2.Count);
for (int i = 0; i < source.Count; i++)
{
@@ -319,13 +319,13 @@ public class MamaTests
var output1 = new double[count];
var output2 = new double[count];
// Call without famaOutput parameter (backwards compatibility)
Mama.Calculate(data, output1);
// Call with empty famaOutput span
Mama.Calculate(data, output2, famaOutput: Span<double>.Empty);
// Both should produce identical MAMA results
for (int i = 0; i < count; i++)
{
@@ -339,8 +339,8 @@ public class MamaTests
var data = new double[10];
var mamaOutput = new double[10];
var famaOutput = new double[5];
var ex = Assert.Throws<ArgumentOutOfRangeException>(() =>
var ex = Assert.Throws<ArgumentOutOfRangeException>(() =>
Mama.Calculate(data, mamaOutput, famaOutput: famaOutput));
Assert.Equal("famaOutput", ex.ParamName);
}
+4 -4
View File
@@ -49,7 +49,7 @@ public class MamaValidationTests
// The optimized version handles quadrants correctly (-pi to pi) and wraps phase differences (-pi to pi),
// while original (and Skender) uses Atan (-pi/2 to pi/2) and ignores phase wrapping, causing divergence.
ValidationHelper.VerifyData(qResult, sResult, x => x.Mama, skip: 100, tolerance: 40.0);
_output.WriteLine("MAMA Batch validated successfully against Skender");
}
@@ -78,7 +78,7 @@ public class MamaValidationTests
// 3. Verify MAMA
// Tolerance increased to 40.0 due to optimized Phase calculation and Phase Wrapping correction.
ValidationHelper.VerifyData(qMamaResults, sResult, x => x.Mama, skip: 100, tolerance: 40.0);
// 4. Verify FAMA
ValidationHelper.VerifyData(qFamaResults, sResult, x => x.Fama, skip: 100, tolerance: 40.0);
@@ -117,14 +117,14 @@ public class MamaValidationTests
// 2. Precision: Ooples uses 4-decimal constants, QuanTAlib uses exact fractions.
// 3. Phase Wrapping: QuanTAlib correctly handles phase wrapping, Ooples does not.
ValidationHelper.VerifyData(qResult, oMama, x => x, skip: 100, tolerance: 40.0);
// 4. Verify FAMA
// QuanTAlib stores Fama in a separate property, not in the main TSeries result
// We need to extract Fama from the indicator instance or capture it during streaming
// But Update(TSeries) returns only the main series (Mama).
// To verify Fama batch, we might need to iterate or expose it.
// For now, let's verify Mama.
_output.WriteLine("MAMA Batch validated successfully against Ooples");
}
}
+1 -1
View File
@@ -158,7 +158,7 @@ Ehlers used `atan(Q/I)` with manual zero-checks because TradeStation's `atan2` d
```csharp
// Period calculation: atan2 handles all quadrants correctly
double angle = Math.Atan2(_state.Im, _state.Re);
double period = Math.Abs(angle) > MinDeltaRadians
double period = Math.Abs(angle) > MinDeltaRadians
? TwoPi / Math.Abs(angle)
: _p_state.Period;
+7 -7
View File
@@ -10,10 +10,10 @@ public class MgdiTests
public void NaN_FirstValue_DoesNotInitializeToZero()
{
var mgdi = new Mgdi(14, 0.6);
// First value is NaN
var result = mgdi.Update(new TValue(DateTime.UtcNow, double.NaN));
// Should be NaN, not 0.0
Assert.True(double.IsNaN(result.Value), $"Expected NaN but got {result.Value}");
}
@@ -22,15 +22,15 @@ public class MgdiTests
public void NaN_Sequence_InitializesOnFirstValid()
{
var mgdi = new Mgdi(14, 0.6);
// Sequence of NaNs
mgdi.Update(new TValue(DateTime.UtcNow, double.NaN));
mgdi.Update(new TValue(DateTime.UtcNow, double.NaN));
// First valid value
double firstValid = 100.0;
var result = mgdi.Update(new TValue(DateTime.UtcNow, firstValid));
Assert.Equal(firstValid, result.Value);
}
@@ -40,7 +40,7 @@ public class MgdiTests
var mgdi = new Mgdi(14, 0.6);
mgdi.Update(new TValue(DateTime.UtcNow, 100.0));
var result = mgdi.Update(new TValue(DateTime.UtcNow, 101.0));
Assert.True(result.Value > 100.0);
Assert.True(result.Value < 101.0);
}
@@ -50,7 +50,7 @@ public class MgdiTests
{
var source = new double[10];
var output = new double[10];
Assert.Throws<ArgumentOutOfRangeException>(() => Mgdi.Calculate(source, output, 14, double.NaN));
Assert.Throws<ArgumentOutOfRangeException>(() => Mgdi.Calculate(source, output, 14, double.PositiveInfinity));
Assert.Throws<ArgumentOutOfRangeException>(() => Mgdi.Calculate(source, output, 14, double.NegativeInfinity));
+1 -1
View File
@@ -58,7 +58,7 @@ public sealed class MgdiValidationTests : IDisposable
// Calculate QuanTAlib MGDI Streaming
var mgdi = new Mgdi(14, 0.6);
var streamingResults = new List<double>();
foreach (var item in _data.Data)
{
streamingResults.Add(mgdi.Update(item).Value);
+1 -1
View File
@@ -361,7 +361,7 @@ public class PwmaTests
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// 1. Batch Mode
var batchSeries = Pwma.Batch(series, period);
double expected = batchSeries.Last.Value;
+1 -1
View File
@@ -208,7 +208,7 @@ public class RmaTests
{
var source = new TSeries();
var rma = new Rma(source, 10);
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, rma.Last.Value, 1e-9);
}
+2 -2
View File
@@ -43,7 +43,7 @@ public sealed class RmaValidationTests : IDisposable
{
// Arrange
int period = 14;
// QuanTAlib RMA
var rma = new Rma(period);
var quantalibResults = new TSeries();
@@ -94,7 +94,7 @@ public sealed class RmaValidationTests : IDisposable
// Skip warmup period for comparison
int skip = period * 30;
int itemsToVerify = _testData.Data.Count - skip;
ValidationHelper.VerifyData(qResult, oValues, (s) => s, skip: itemsToVerify, tolerance: ValidationHelper.OoplesTolerance);
}
}
+11 -11
View File
@@ -472,7 +472,7 @@ public class SmaTests
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// 1. Batch Mode
var batchSeries = Sma.Batch(series, period);
double expected = batchSeries.Last.Value;
@@ -512,7 +512,7 @@ public class SmaTests
{
var source = new TSeries();
var sma = new Sma(source, 10);
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, sma.Last.Value);
}
@@ -529,12 +529,12 @@ public class SmaTests
{
var sma = new Sma(5);
double[] history = [10, 20, 30, 40, 50]; // SMA(5) = 30
sma.Prime(history);
Assert.True(sma.IsHot);
Assert.Equal(30.0, sma.Last.Value, 1e-10);
// Verify it continues correctly
sma.Update(new TValue(DateTime.UtcNow, 60)); // 20,30,40,50,60 -> 40
Assert.Equal(40.0, sma.Last.Value, 1e-10);
@@ -544,8 +544,8 @@ public class SmaTests
public void Prime_WithInsufficientHistory_IsNotHot()
{
var sma = new Sma(10);
double[] history = [10, 20, 30, 40, 50];
double[] history = [10, 20, 30, 40, 50];
sma.Prime(history);
Assert.False(sma.IsHot);
@@ -556,14 +556,14 @@ public class SmaTests
public void Prime_HandlesNaN_InHistory()
{
var sma = new Sma(3);
double[] history = [10, 20, double.NaN, 40];
double[] history = [10, 20, double.NaN, 40];
// 10
// 10, 20
// 10, 20, 20 (NaN replaced by 20) -> Avg(10,20,20) = 16.666...
// 20, 20, 40 -> Avg(20,20,40) = 26.666...
sma.Prime(history);
Assert.True(sma.IsHot);
Assert.Equal(80.0 / 3.0, sma.Last.Value, 1e-9);
}
@@ -574,7 +574,7 @@ public class SmaTests
var series = new TSeries();
for (int i = 1; i <= 10; i++) series.Add(DateTime.UtcNow, i * 10);
// 10, 20, 30, 40, 50, 60, 70, 80, 90, 100
// SMA(5)
var (results, indicator) = Sma.Calculate(series, 5);
@@ -589,7 +589,7 @@ public class SmaTests
Assert.Equal(5, indicator.WarmupPeriod);
// Verify indicator continues correctly
indicator.Update(new TValue(DateTime.UtcNow, 110));
indicator.Update(new TValue(DateTime.UtcNow, 110));
// Window was [60, 70, 80, 90, 100] -> Avg 80
// New Window [70, 80, 90, 100, 110] -> Avg 90
Assert.Equal(90.0, indicator.Last.Value);
+1 -1
View File
@@ -29,7 +29,7 @@ public sealed class SmaToleranceTests : IDisposable
var sResult = _testData.SkenderQuotes.GetSma(period).ToList();
ValidationHelper.VerifyData(qResult, sResult, (s) => s.Sma);
// Add explicit assertion to satisfy SonarQube
Assert.True(qResult.Count > 0);
}
+5 -5
View File
@@ -10,13 +10,13 @@ public class SmaZeroDivTests
public void Sma_Update_WithIsNewFalse_OnEmptyBuffer_DoesNotThrow()
{
var sma = new Sma(10);
// Buffer is empty initially.
// Calling Update with isNew=false should not cause division by zero.
// It should return NaN or 0 or Last, but definitely not throw or return Infinity.
var result = sma.Update(new TValue(DateTime.UtcNow, 100), isNew: false);
// Since buffer count is 0, we expect NaN based on our fix.
Assert.True(double.IsNaN(result.Value), $"Expected NaN but got {result.Value}");
}
@@ -27,10 +27,10 @@ public class SmaZeroDivTests
var sma = new Sma(10);
sma.Update(new TValue(DateTime.UtcNow, 100));
sma.Reset();
// Buffer is empty after Reset.
var result = sma.Update(new TValue(DateTime.UtcNow, 200), isNew: false);
Assert.True(double.IsNaN(result.Value), $"Expected NaN but got {result.Value}");
}
}
+3 -3
View File
@@ -189,7 +189,7 @@ public sealed class Sma : AbstractBase
{
// Capture previous state BEFORE any mutation
_p_state = _state;
double val = GetValidValue(input.Value);
UpdateState(val);
_state.LastInput = val;
@@ -199,11 +199,11 @@ public sealed class Sma : AbstractBase
{
// Restore scalar state to pre-mutation values
_state = _p_state;
double val = GetValidValue(input.Value);
// Update sum: remove the value that was added during isNew=true, add the new correction value
_state.Sum = _state.Sum - _currentBarValue + val;
// Update the buffer's newest value and sync its internal sum with our state sum
_buffer.UpdateNewest(val);
_state.Sum = _buffer.RecalculateSum(); // Ensure sums stay in sync
+1 -1
View File
@@ -249,7 +249,7 @@ public class SsfTests
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// 1. Batch Mode
var batchSeries = Ssf.Calculate(series, period).Results;
double expected = batchSeries.Last.Value;
+5 -5
View File
@@ -178,11 +178,11 @@ public sealed class Ssf : AbstractBase
double ssf = (_state.Count < 4)
? val
: (_c1 * (val + _state.PrevInput) * 0.5) + (_c2 * _state.Ssf1) + (_c3 * _state.Ssf2);
_state.Ssf2 = _state.Ssf1;
_state.Ssf1 = ssf;
_state.PrevInput = val;
if (isNew) _state.Count++;
if (!_state.IsHot && _state.Count >= WarmupPeriod)
_state.IsHot = true;
@@ -216,7 +216,7 @@ public sealed class Ssf : AbstractBase
sourceTimes.CopyTo(tSpan);
_p_state = _state;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v);
@@ -239,12 +239,12 @@ public sealed class Ssf : AbstractBase
state.Ssf1 = state.LastValidValue;
state.Ssf2 = state.LastValidValue;
state.PrevInput = state.LastValidValue;
output[i] = state.LastValidValue;
output[i] = state.LastValidValue;
state.Count = 1;
i++;
break;
}
output[i] = double.NaN;
output[i] = double.NaN;
}
// Handle all-NaN case: if no finite value was found, set remaining outputs to NaN and return
+1 -1
View File
@@ -54,7 +54,7 @@ public sealed class SuperIndicator : Indicator, IWatchlistIndicator
bool isNew = args.IsNewBar();
var bar = this.GetInputBar(args);
double value = _super!.Update(bar, isNew).Value;
_series!.SetValue(value, _super.IsHot, ShowColdValues);
_upperBand!.SetValue(_super.UpperBand.Value, _super.IsHot, ShowColdValues);
_lowerBand!.SetValue(_super.LowerBand.Value, _super.IsHot, ShowColdValues);
+10 -10
View File
@@ -70,13 +70,13 @@ public class SuperTests
super.Reset();
Assert.Equal(0, super.Last.Value);
Assert.False(super.IsHot);
// Feed again
for (int i = 0; i < bars.Count; i++)
{
super.Update(bars[i]);
}
Assert.True(double.IsFinite(super.Last.Value));
}
@@ -110,14 +110,14 @@ public class SuperTests
}
}
}
[Fact]
public void Warmup_Handling()
{
var super = new Super(10, 3.0);
var gbm = new GBM();
var bars = gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// First 10 bars should be NaN
for (int i = 0; i < 10; i++)
{
@@ -125,7 +125,7 @@ public class SuperTests
Assert.True(double.IsNaN(result.Value), $"Bar {i} should be NaN");
Assert.False(super.IsHot);
}
// 11th bar (index 10) should be valid
var result11 = super.Update(bars[10]);
Assert.True(double.IsFinite(result11.Value), "Bar 10 should be finite");
@@ -146,16 +146,16 @@ public class SuperTests
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var super = new Super(10, 3.0);
var streamingResults = new List<double>();
for (int i = 0; i < bars.Count; i++)
{
streamingResults.Add(super.Update(bars[i]).Value);
}
var staticResults = Super.Batch(bars, 10, 3.0);
Assert.Equal(streamingResults.Count, staticResults.Count);
for (int i = 0; i < staticResults.Count; i++)
{
@@ -176,12 +176,12 @@ public class SuperTests
var super = new Super(10, 3.0);
var gbm = new GBM();
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Test TBarSeries chain
var result = super.Update(bars);
Assert.NotNull(result);
Assert.IsType<TSeries>(result);
// Test TBar chain (returns TValue)
var result2 = super.Update(bars[0]);
Assert.IsType<TValue>(result2);
+21 -21
View File
@@ -66,13 +66,13 @@ public class T3Tests
t3.Reset();
Assert.Equal(0, t3.Last.Value);
Assert.False(t3.IsHot);
// Feed again
for (int i = 0; i < bars.Count; i++)
{
t3.Update(new TValue(bars[i].Time, bars[i].Close));
}
Assert.True(double.IsFinite(t3.Last.Value));
}
@@ -99,23 +99,23 @@ public class T3Tests
Assert.Equal(streamingResults[i], seriesResults.Values[i], 1e-9);
}
}
[Fact]
public void BatchCalculate_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var t3 = new T3(5, 0.7);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(t3.Update(series[i]).Value);
}
var batchResults = T3.Batch(series, 5, 0.7);
Assert.Equal(streamingResults.Count, batchResults.Count);
for (int i = 0; i < batchResults.Count; i++)
{
@@ -129,17 +129,17 @@ public class T3Tests
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var t3 = new T3(5, 0.7);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(t3.Update(series[i]).Value);
}
var spanResults = new double[series.Count];
T3.Batch(series.Values, spanResults, 5, 0.7);
for (int i = 0; i < spanResults.Length; i++)
{
Assert.Equal(streamingResults[i], spanResults[i], 1e-9);
@@ -153,12 +153,12 @@ public class T3Tests
var gbm = new GBM();
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// Test TSeries chain
var result = t3.Update(series);
Assert.NotNull(result);
Assert.IsType<TSeries>(result);
// Test TValue chain
var result2 = t3.Update(series[0]);
Assert.IsType<TValue>(result2);
@@ -255,13 +255,13 @@ public class T3Tests
{
var input = new double[10];
var output = new double[10];
var ex1 = Assert.Throws<ArgumentOutOfRangeException>(() => T3.Batch(input, output, 5, 0.0));
Assert.Equal("vfactor", ex1.ParamName);
var ex2 = Assert.Throws<ArgumentOutOfRangeException>(() => T3.Batch(input, output, 5, -0.5));
Assert.Equal("vfactor", ex2.ParamName);
var ex3 = Assert.Throws<ArgumentOutOfRangeException>(() => T3.Batch(input, output, 5, 1.5));
Assert.Equal("vfactor", ex3.ParamName);
}
@@ -277,7 +277,7 @@ public class T3Tests
{
var source = new TestPublisher();
_ = new T3(source, 5);
Assert.Equal(1, source.SubscriberCount);
}
@@ -286,11 +286,11 @@ public class T3Tests
{
var source = new TestPublisher();
var t3 = new T3(source, 5);
Assert.Equal(1, source.SubscriberCount);
t3.Dispose();
Assert.Equal(0, source.SubscriberCount);
}
@@ -299,12 +299,12 @@ public class T3Tests
{
var source = new TestPublisher();
var t3 = new T3(source, 5);
t3.Dispose();
#pragma warning disable S3966 // Objects should not be disposed more than once
t3.Dispose();
#pragma warning restore S3966 // Objects should not be disposed more than once
Assert.Equal(0, source.SubscriberCount);
}
@@ -312,7 +312,7 @@ public class T3Tests
public void Dispose_DoesNothing_WhenNoSource()
{
var t3 = new T3(5);
var exception = Record.Exception(() => t3.Dispose());
Assert.Null(exception);
}
+5 -5
View File
@@ -8,19 +8,19 @@ namespace QuanTAlib;
/// T3: Tillson T3 Moving Average
/// </summary>
/// <remarks>
/// T3 works by running price data through a series of six EMAs, then combining the outputs
/// T3 works by running price data through a series of six EMAs, then combining the outputs
/// of these EMAs using carefully calculated weights.
///
///
/// Formula:
/// T3 = c1*e6 + c2*e5 + c3*e4 + c4*e3
///
///
/// Where:
/// e1..e6 are cascaded EMAs
/// c1 = -v^3
/// c2 = 3(v^2 + v^3)
/// c3 = -3(2v^2 + v + v^3)
/// c4 = 1 + 3v + 3v^2 + v^3
///
///
/// v is volume factor (default 0.7)
/// alpha = 2 / (period + 1)
/// </remarks>
@@ -154,7 +154,7 @@ public sealed class T3 : AbstractBase, IDisposable
// Since Compute returns the result but also updates state, we can't easily get the last result without re-running or storing it.
// However, Prime is usually followed by Update or we just need the state ready.
// If we want Last to be correct, we should probably store the last result.
// But AbstractBase.Prime doesn't strictly require Last to be set to the very last value of source,
// But AbstractBase.Prime doesn't strictly require Last to be set to the very last value of source,
// though it's good practice.
// Let's re-run the last value computation to set Last correctly.
if (len > 0)
+1 -1
View File
@@ -68,7 +68,7 @@ public class TemaIndicatorTests
for (int i = 0; i < 10; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 102);
// Process update
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
+11 -11
View File
@@ -66,13 +66,13 @@ public class TemaTests
tema.Reset();
Assert.Equal(0, tema.Last.Value);
Assert.False(tema.IsHot);
// Feed again
for (int i = 0; i < bars.Count; i++)
{
tema.Update(new TValue(bars[i].Time, bars[i].Close));
}
Assert.True(double.IsFinite(tema.Last.Value));
}
@@ -99,23 +99,23 @@ public class TemaTests
Assert.Equal(streamingResults[i], seriesResults.Values[i], 1e-9);
}
}
[Fact]
public void BatchCalculate_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var tema = new Tema(10);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(tema.Update(series[i]).Value);
}
var batchResults = Tema.Batch(series, 10);
Assert.Equal(streamingResults.Count, batchResults.Count);
for (int i = 0; i < batchResults.Count; i++)
{
@@ -129,17 +129,17 @@ public class TemaTests
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var tema = new Tema(10);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(tema.Update(series[i]).Value);
}
var spanResults = new double[series.Count];
Tema.Batch(series.Values, spanResults, 10);
for (int i = 0; i < spanResults.Length; i++)
{
Assert.Equal(streamingResults[i], spanResults[i], 1e-9);
@@ -153,12 +153,12 @@ public class TemaTests
var gbm = new GBM();
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// Test TSeries chain
var result = tema.Update(series);
Assert.NotNull(result);
Assert.IsType<TSeries>(result);
// Test TValue chain
var result2 = tema.Update(series[0]);
Assert.IsType<TValue>(result2);
+2 -2
View File
@@ -85,9 +85,9 @@ public class TemaValidationTests
var temaIndicator = Tulip.Indicators.tema;
double[][] inputs = { _testData.RawData.ToArray() };
double[] options = { period };
// Tulip TEMA lookback is 3*(period-1)
int lookback = 3 * (period - 1);
int lookback = 3 * (period - 1);
double[][] outputs = { new double[_testData.RawData.Length - lookback] };
temaIndicator.Run(inputs, options, outputs);
+11 -11
View File
@@ -66,13 +66,13 @@ public class TrimaTests
trima.Reset();
Assert.Equal(0, trima.Last.Value);
Assert.False(trima.IsHot);
// Feed again
for (int i = 0; i < bars.Count; i++)
{
trima.Update(new TValue(bars[i].Time, bars[i].Close));
}
Assert.True(double.IsFinite(trima.Last.Value));
}
@@ -99,23 +99,23 @@ public class TrimaTests
Assert.Equal(streamingResults[i], seriesResults.Values[i], 1e-9);
}
}
[Fact]
public void BatchCalculate_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var trima = new Trima(10);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(trima.Update(series[i]).Value);
}
var batchResults = Trima.Batch(series, 10);
Assert.Equal(streamingResults.Count, batchResults.Count);
for (int i = 0; i < batchResults.Count; i++)
{
@@ -129,17 +129,17 @@ public class TrimaTests
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var trima = new Trima(10);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(trima.Update(series[i]).Value);
}
var spanResults = new double[series.Count];
Trima.Batch(series.Values, spanResults, 10);
for (int i = 0; i < spanResults.Length; i++)
{
Assert.Equal(streamingResults[i], spanResults[i], 1e-9);
@@ -153,12 +153,12 @@ public class TrimaTests
var gbm = new GBM();
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// Test TSeries chain
var result = trima.Update(series);
Assert.NotNull(result);
Assert.IsType<TSeries>(result);
// Test TValue chain
var result2 = trima.Update(series[0]);
Assert.IsType<TValue>(result2);
+7 -7
View File
@@ -36,14 +36,14 @@ public class TrimaValidationTests
int p2 = (period + 1) / 2;
var sma1Results = _testData.SkenderQuotes.GetSma(p1).ToList();
// Map SMA1 results to Quotes for the second pass
// Note: We use 0 for null values during warmup, which might affect early values
// but should stabilize for the verification window (last 100 records)
var quotes2 = sma1Results.Select(r => new Quote
{
Date = r.Date,
Close = (decimal)(r.Sma ?? 0)
var quotes2 = sma1Results.Select(r => new Quote
{
Date = r.Date,
Close = (decimal)(r.Sma ?? 0)
}).ToList();
var sResult = quotes2.GetSma(p2).ToList();
@@ -99,10 +99,10 @@ public class TrimaValidationTests
// Usually it's period-1 for simple averages, but TRIMA is double smoothed.
// We'll rely on the output length to align.
// Tulip.Indicators.trima.Run expects outputs to be sized correctly.
// We can try to run it with a large buffer and see what happens,
// We can try to run it with a large buffer and see what happens,
// or calculate the expected lookback.
// For TRIMA(n), lookback is roughly n-1.
int lookback = period - 1;
int lookback = period - 1;
double[][] outputs = { new double[_testData.RawData.Length - lookback] };
trimaIndicator.Run(inputs, options, outputs);
+2 -2
View File
@@ -19,10 +19,10 @@ public class UsfIndicatorTests
public void Indicator_ProcessesData()
{
var indicator = new UsfIndicator();
// Simulate Init
indicator.GetType().GetMethod("OnInit", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.Invoke(indicator, null);
Assert.NotNull(indicator);
}
}
+11 -11
View File
@@ -66,13 +66,13 @@ public class UsfTests
usf.Reset();
Assert.Equal(0, usf.Last.Value);
Assert.False(usf.IsHot);
// Feed again
for (int i = 0; i < bars.Count; i++)
{
usf.Update(new TValue(bars[i].Time, bars[i].Close));
}
Assert.True(double.IsFinite(usf.Last.Value));
}
@@ -99,23 +99,23 @@ public class UsfTests
Assert.Equal(streamingResults[i], seriesResults.Values[i], 1e-9);
}
}
[Fact]
public void BatchCalculate_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var usf = new Usf(10);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(usf.Update(series[i]).Value);
}
var batchResults = Usf.Calculate(series, 10).Results;
Assert.Equal(streamingResults.Count, batchResults.Count);
for (int i = 0; i < batchResults.Count; i++)
{
@@ -129,17 +129,17 @@ public class UsfTests
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var usf = new Usf(10);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(usf.Update(series[i]).Value);
}
var spanResults = new double[series.Count];
Usf.Calculate(series.Values, spanResults, 10);
for (int i = 0; i < spanResults.Length; i++)
{
Assert.Equal(streamingResults[i], spanResults[i], 1e-9);
@@ -153,12 +153,12 @@ public class UsfTests
var gbm = new GBM();
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// Test TSeries chain
var result = usf.Update(series);
Assert.NotNull(result);
Assert.IsType<TSeries>(result);
// Test TValue chain
var result2 = usf.Update(series[0]);
Assert.IsType<TValue>(result2);
+5 -5
View File
@@ -168,12 +168,12 @@ public sealed class Usf : AbstractBase
double usf = (_state.Count < 4)
? val
: (1.0 - _c1) * val + (2.0 * _c1 - _c2) * _state.PrevInput1 - (_c1 + _c3) * _state.PrevInput2 + _c2 * _state.Usf1 + _c3 * _state.Usf2;
_state.Usf2 = _state.Usf1;
_state.Usf1 = usf;
_state.PrevInput2 = _state.PrevInput1;
_state.PrevInput1 = val;
if (isNew && !initialized) _state.Count++;
if (!_state.IsHot && _state.Count >= WarmupPeriod)
_state.IsHot = true;
@@ -207,7 +207,7 @@ public sealed class Usf : AbstractBase
sourceTimes.CopyTo(tSpan);
_p_state = _state;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v);
@@ -231,12 +231,12 @@ public sealed class Usf : AbstractBase
state.Usf2 = state.LastValidValue;
state.PrevInput1 = state.LastValidValue;
state.PrevInput2 = state.LastValidValue;
output[i] = state.LastValidValue;
output[i] = state.LastValidValue;
state.Count = 1;
i++;
break;
}
output[i] = double.NaN;
output[i] = double.NaN;
}
}
+1 -1
View File
@@ -59,7 +59,7 @@ public class VidyaIndicatorTests
for (int i = 0; i < 10; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 102);
// Process update
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
+11 -11
View File
@@ -65,13 +65,13 @@ public class VidyaTests
vidya.Reset();
Assert.Equal(0, vidya.Last.Value);
// Feed again
for (int i = 0; i < bars.Count; i++)
{
vidya.Update(new TValue(bars[i].Time, bars[i].Close));
}
Assert.True(double.IsFinite(vidya.Last.Value));
}
@@ -98,23 +98,23 @@ public class VidyaTests
Assert.Equal(streamingResults[i], seriesResults.Values[i], 1e-9);
}
}
[Fact]
public void BatchCalculate_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var vidya = new Vidya(10);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(vidya.Update(series[i]).Value);
}
var batchResults = Vidya.Batch(series, 10);
Assert.Equal(streamingResults.Count, batchResults.Count);
for (int i = 0; i < batchResults.Count; i++)
{
@@ -128,17 +128,17 @@ public class VidyaTests
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var vidya = new Vidya(10);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(vidya.Update(series[i]).Value);
}
var spanResults = new double[series.Count];
Vidya.Batch(series.Values, spanResults, 10);
for (int i = 0; i < spanResults.Length; i++)
{
Assert.Equal(streamingResults[i], spanResults[i], 1e-9);
@@ -152,12 +152,12 @@ public class VidyaTests
var gbm = new GBM();
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// Test TSeries chain
var result = vidya.Update(series);
Assert.NotNull(result);
Assert.IsType<TSeries>(result);
// Test TValue chain
var result2 = vidya.Update(series[0]);
Assert.IsType<TValue>(result2);
+3 -3
View File
@@ -201,7 +201,7 @@ public sealed class Vidya : AbstractBase, IDisposable
// Process all data to build up state
// For recursive indicators like VIDYA, we generally need to process from the start
// or at least a significant warmup period.
// Given we don't know the "correct" previous VIDYA without processing,
// Given we don't know the "correct" previous VIDYA without processing,
// we process the whole provided history.
double prevClose = source[0];
@@ -254,7 +254,7 @@ public sealed class Vidya : AbstractBase, IDisposable
_state.LastVidya = lastVidya;
// Set Last
// Note: Time is not available in Span, so we use MinValue.
// Note: Time is not available in Span, so we use MinValue.
// It will be updated on next Update.
Last = new TValue(DateTime.MinValue, _state.CurrentVidya);
_p_state = _state;
@@ -286,7 +286,7 @@ public sealed class Vidya : AbstractBase, IDisposable
double alpha = 2.0 / (period + 1);
// Use arrays for buffers to avoid heap allocations if possible,
// Use arrays for buffers to avoid heap allocations if possible,
// but period is dynamic.
// We can use ArrayPool or just new double[period] if period is small.
// For simplicity and safety with large periods, let's use ArrayPool.
+9 -9
View File
@@ -17,7 +17,7 @@ public class WmaCoverageTests
for (int i = 0; i < len; i++) source[i] = i;
double[] output = new double[len];
// This should trigger CalculateScalarCore internally
Wma.Batch(source.AsSpan(), output.AsSpan(), period);
@@ -45,26 +45,26 @@ public class WmaCoverageTests
// However, we can't easily invoke it directly.
// But wait, I previously wrote a test that called a *copy* of the method.
// Calling the *actual* private method with Spans via reflection is not possible in C# (TargetInvocationException).
// Strategy change:
// Strategy change:
// Since we cannot invoke private methods with Span args via reflection,
// and we cannot change the visibility of the methods (they should remain private),
// we are limited in how we can "force" coverage of the private AVX2 method if AVX512 is present.
// However, we CAN use the fact that Wma.Batch checks for Avx512F.IsSupported.
// We cannot change that runtime flag.
// Actually, we can't easily cover the AVX2 path on an AVX512 machine without code modification or a "TestAccessor" pattern.
// But wait, the user asked "why is coverage only 46%".
// If I can't run the code, I can't cover it.
// BUT, I can verify the Scalar Core logic by using the small data test (done above).
// For AVX2, if I can't invoke it, I can't cover it on this machine.
// Let's double check if there's any way to invoke it.
// Maybe I can use `MethodInfo.CreateDelegate`?
// Delegates can take Spans if defined correctly.
InvokePrivateStaticMethod_WithSpans("CalculateSimdCore", source, output, period);
}
catch (Exception ex)
@@ -98,7 +98,7 @@ public class WmaCoverageTests
// Create a delegate that matches the signature
// Note: ReadOnlySpan<double> and Span<double> in delegate signature
var del = methodInfo.CreateDelegate<CoreDelegate>();
del(source.AsSpan(), output.AsSpan(), period);
}
}
+1 -1
View File
@@ -68,7 +68,7 @@ public class WmaIndicatorTests
for (int i = 0; i < 10; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 102);
// Process update
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
+23 -23
View File
@@ -66,13 +66,13 @@ public class WmaTests
wma.Reset();
Assert.Equal(0, wma.Last.Value);
Assert.False(wma.IsHot);
// Feed again
for (int i = 0; i < bars.Count; i++)
{
wma.Update(new TValue(bars[i].Time, bars[i].Close));
}
Assert.True(double.IsFinite(wma.Last.Value));
}
@@ -99,23 +99,23 @@ public class WmaTests
Assert.Equal(streamingResults[i], seriesResults.Values[i], 1e-9);
}
}
[Fact]
public void StaticBatch_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var wma = new Wma(10);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(wma.Update(series[i]).Value);
}
var staticResults = Wma.Batch(series, 10);
Assert.Equal(streamingResults.Count, staticResults.Count);
for (int i = 0; i < staticResults.Count; i++)
{
@@ -129,17 +129,17 @@ public class WmaTests
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var wma = new Wma(10);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(wma.Update(series[i]).Value);
}
var spanResults = new double[series.Count];
Wma.Batch(series.Values, spanResults, 10);
for (int i = 0; i < spanResults.Length; i++)
{
Assert.Equal(streamingResults[i], spanResults[i], 1e-9);
@@ -153,12 +153,12 @@ public class WmaTests
var gbm = new GBM();
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// Test TSeries chain
var result = wma.Update(series);
Assert.NotNull(result);
Assert.IsType<TSeries>(result);
// Test TValue chain
var result2 = wma.Update(series[0]);
Assert.IsType<TValue>(result2);
@@ -176,14 +176,14 @@ public class WmaTests
{
var source = new TSeries();
var wma = new Wma(source, 10);
// Verify subscription works
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, wma.Last.Value);
// Dispose
wma.Dispose();
// Verify unsubscription
source.Add(new TValue(DateTime.UtcNow, 200));
// Last value should remain unchanged if unsubscribed
@@ -251,11 +251,11 @@ public class WmaTests
public void Update_IsNewFalse_OnEmptyBuffer_ThrowsInvalidOperationException()
{
var wma = new Wma(10);
// Calling Update with isNew=false on an empty buffer should throw
var exception = Assert.Throws<InvalidOperationException>(() =>
wma.Update(new TValue(DateTime.UtcNow, 100.0), isNew: false));
Assert.Contains("isNew=false", exception.Message, StringComparison.Ordinal);
Assert.Contains("buffer is empty", exception.Message, StringComparison.Ordinal);
Assert.Contains("isNew=true", exception.Message, StringComparison.Ordinal);
@@ -267,20 +267,20 @@ public class WmaTests
var wma = new Wma(10);
var gbm = new GBM();
var bars = gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Feed some data
for (int i = 0; i < 10; i++)
{
wma.Update(new TValue(bars[i].Time, bars[i].Close));
}
// Reset clears the buffer
wma.Reset();
// Calling Update with isNew=false after reset should throw
var exception = Assert.Throws<InvalidOperationException>(() =>
wma.Update(new TValue(bars[10].Time, bars[10].Close), isNew: false));
Assert.Contains("isNew=false", exception.Message, StringComparison.Ordinal);
Assert.Contains("buffer is empty", exception.Message, StringComparison.Ordinal);
}
@@ -291,16 +291,16 @@ public class WmaTests
var wma = new Wma(10);
var gbm = new GBM();
var bars = gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Feed some data first
for (int i = 0; i < 5; i++)
{
wma.Update(new TValue(bars[i].Time, bars[i].Close), isNew: true);
}
// Now isNew=false should work (buffer has data)
var result = wma.Update(new TValue(bars[4].Time, bars[4].Close + 10), isNew: false);
// Should not throw and should return a finite value
Assert.True(double.IsFinite(result.Value));
}