mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-19 11:08:05 +00:00
feat(rsi, alma, bilateral, blma, butter, ema, htit, kama, lsma, pwma, rma, trima, vidya, wma): enhance calculations with NaN handling and edge case management; improve performance and stability across multiple classes
This commit is contained in:
@@ -98,9 +98,10 @@ public sealed class Rsi : AbstractBase
|
||||
double avgLoss = _avgLoss.Update(new TValue(input.Time, loss), isNew).Value;
|
||||
|
||||
double rsi;
|
||||
if (avgLoss == 0)
|
||||
const double epsilon = 1e-10;
|
||||
if (avgLoss < epsilon)
|
||||
{
|
||||
rsi = (avgGain == 0) ? 50 : 100;
|
||||
rsi = (avgGain < epsilon) ? 50 : 100;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -227,7 +228,7 @@ public sealed class Rsi : AbstractBase
|
||||
var v100 = new Vector<double>(100.0);
|
||||
var v1 = Vector<double>.One;
|
||||
var v50 = new Vector<double>(50.0);
|
||||
var vZero = Vector<double>.Zero;
|
||||
var vEpsilon = new Vector<double>(1e-10);
|
||||
|
||||
for (; i <= len - vectorSize; i += vectorSize)
|
||||
{
|
||||
@@ -239,8 +240,8 @@ public sealed class Rsi : AbstractBase
|
||||
var vRsi = v100 - (v100 / (v1 + vRs));
|
||||
|
||||
// Handle edge cases where loss is zero
|
||||
var vLossIsZero = Vector.Equals(vLoss, vZero);
|
||||
var vGainIsZero = Vector.Equals(vGain, vZero);
|
||||
var vLossIsZero = Vector.LessThan(vLoss, vEpsilon);
|
||||
var vGainIsZero = Vector.LessThan(vGain, vEpsilon);
|
||||
|
||||
// If loss is zero:
|
||||
// If gain is also zero -> 50
|
||||
@@ -257,14 +258,15 @@ public sealed class Rsi : AbstractBase
|
||||
}
|
||||
}
|
||||
|
||||
const double epsilon = 1e-10;
|
||||
for (; i < len; i++)
|
||||
{
|
||||
double avgGain = gainSpan[i];
|
||||
double avgLoss = lossSpan[i];
|
||||
|
||||
if (avgLoss == 0)
|
||||
if (avgLoss < epsilon)
|
||||
{
|
||||
output[i] = (avgGain == 0) ? 50 : 100;
|
||||
output[i] = (avgGain < epsilon) ? 50 : 100;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
+11
-2
@@ -104,6 +104,12 @@ public sealed class Alma : AbstractBase, IDisposable
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
return Update(input, isNew, true);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private TValue Update(TValue input, bool isNew, bool publish)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
@@ -129,7 +135,10 @@ public sealed class Alma : AbstractBase, IDisposable
|
||||
}
|
||||
|
||||
Last = new TValue(input.Time, result);
|
||||
PubEvent(Last);
|
||||
if (publish)
|
||||
{
|
||||
PubEvent(Last);
|
||||
}
|
||||
return Last;
|
||||
}
|
||||
|
||||
@@ -157,7 +166,7 @@ public sealed class Alma : AbstractBase, IDisposable
|
||||
int startIndex = Math.Max(0, len - _period);
|
||||
for (int i = startIndex; i < len; i++)
|
||||
{
|
||||
Update(source[i]);
|
||||
Update(source[i], true, false);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
|
||||
@@ -170,7 +170,7 @@ public class BilateralValidationTests : IDisposable
|
||||
sumWeightedSrc += weight * valI;
|
||||
}
|
||||
|
||||
return sumWeights == 0.0 ? centerVal : sumWeightedSrc / sumWeights;
|
||||
return sumWeights < 1e-10 ? centerVal : sumWeightedSrc / sumWeights;
|
||||
}
|
||||
|
||||
private static double CalculateStDev(List<double> values)
|
||||
|
||||
@@ -239,7 +239,7 @@ public sealed class Bilateral : AbstractBase
|
||||
sumWeightedSrc += weight * val;
|
||||
}
|
||||
|
||||
return sumWeights == 0.0 ? centerVal : sumWeightedSrc / sumWeights;
|
||||
return sumWeights < 1e-10 ? centerVal : sumWeightedSrc / sumWeights;
|
||||
}
|
||||
|
||||
private void PrecalculateSpatialWeights()
|
||||
@@ -362,7 +362,7 @@ public sealed class Bilateral : AbstractBase
|
||||
sumWeightedSrc += weight * wVal;
|
||||
}
|
||||
|
||||
destination[i] = sumWeights == 0.0 ? centerVal : sumWeightedSrc / sumWeights;
|
||||
destination[i] = sumWeights < 1e-10 ? centerVal : sumWeightedSrc / sumWeights;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+18
-7
@@ -5,12 +5,14 @@ using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public sealed class Blma : AbstractBase
|
||||
public sealed class Blma : AbstractBase, IDisposable
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly RingBuffer _buffer;
|
||||
private readonly double[] _weights;
|
||||
private readonly double _weightSum;
|
||||
private ITValuePublisher? _publisher;
|
||||
private bool _hasLast;
|
||||
|
||||
public override bool IsHot => _buffer.Count >= _period;
|
||||
|
||||
@@ -33,14 +35,12 @@ public sealed class Blma : AbstractBase
|
||||
|
||||
public Blma(object source, int period) : this(period)
|
||||
{
|
||||
if (source is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
|
||||
if (source is ITValuePublisher pub)
|
||||
{
|
||||
pub.Pub += Handle;
|
||||
_publisher = pub;
|
||||
_publisher.Pub += Handle;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -48,6 +48,15 @@ public sealed class Blma : AbstractBase
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_publisher != null)
|
||||
{
|
||||
_publisher.Pub -= Handle;
|
||||
_publisher = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void Handle(TValue value)
|
||||
{
|
||||
Update(value);
|
||||
@@ -56,6 +65,7 @@ public sealed class Blma : AbstractBase
|
||||
public override void Reset()
|
||||
{
|
||||
_buffer.Clear();
|
||||
_hasLast = false;
|
||||
}
|
||||
|
||||
public override void Prime(ReadOnlySpan<double> source)
|
||||
@@ -80,7 +90,7 @@ public sealed class Blma : AbstractBase
|
||||
{
|
||||
if (double.IsNaN(input.Value) || double.IsInfinity(input.Value))
|
||||
{
|
||||
return Last;
|
||||
return _hasLast ? Last : default;
|
||||
}
|
||||
|
||||
_buffer.Add(input.Value, isNew);
|
||||
@@ -116,6 +126,7 @@ public sealed class Blma : AbstractBase
|
||||
|
||||
var tValue = new TValue(input.Time, result);
|
||||
Last = tValue;
|
||||
_hasLast = true;
|
||||
PubEvent(tValue);
|
||||
return tValue;
|
||||
}
|
||||
|
||||
@@ -78,9 +78,10 @@ public sealed class Butter : AbstractBase
|
||||
|
||||
public override void Prime(ReadOnlySpan<double> source)
|
||||
{
|
||||
foreach (var value in source)
|
||||
DateTime baseTime = DateTime.UtcNow;
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
Update(new TValue(DateTime.UtcNow, value));
|
||||
Update(new TValue(baseTime.AddTicks(i), source[i]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -590,6 +590,21 @@ public class EmaTests
|
||||
Assert.Equal(verifyEma.Last.Value, indicator.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_Batch_AllNaNs_ReturnsNaN()
|
||||
{
|
||||
double[] source = [double.NaN, double.NaN, double.NaN];
|
||||
double[] output = new double[3];
|
||||
|
||||
Ema.Batch(source.AsSpan(), output.AsSpan(), 5);
|
||||
|
||||
// Should be all NaNs, not 0s
|
||||
foreach (var val in output)
|
||||
{
|
||||
Assert.True(double.IsNaN(val), $"Expected NaN but got {val}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_AllModes_ProduceSameResult()
|
||||
{
|
||||
|
||||
@@ -382,6 +382,7 @@ public sealed class Ema : AbstractBase
|
||||
|
||||
var state = State.New();
|
||||
double lastValid = 0;
|
||||
bool foundValid = false;
|
||||
|
||||
// Find first valid value to seed lastValid
|
||||
for (int k = 0; k < source.Length; k++)
|
||||
@@ -389,10 +390,17 @@ public sealed class Ema : AbstractBase
|
||||
if (double.IsFinite(source[k]))
|
||||
{
|
||||
lastValid = source[k];
|
||||
foundValid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundValid)
|
||||
{
|
||||
output.Fill(double.NaN);
|
||||
return;
|
||||
}
|
||||
|
||||
CalculateCore(source, output, alpha, ref state, ref lastValid);
|
||||
}
|
||||
|
||||
|
||||
@@ -377,7 +377,8 @@ public sealed class Htit : AbstractBase
|
||||
smoothPeriod = 0.33 * period + 0.67 * p_smoothPeriod;
|
||||
|
||||
// 8. Instantaneous Trend
|
||||
int dcPeriods = (int)(smoothPeriod + 0.5);
|
||||
double safeSmooth = double.IsNaN(smoothPeriod) ? 0 : smoothPeriod;
|
||||
int dcPeriods = (int)(safeSmooth + 0.5);
|
||||
double sumPr = 0;
|
||||
int prCount = 0;
|
||||
|
||||
|
||||
+1
-14
@@ -22,7 +22,6 @@ namespace QuanTAlib;
|
||||
[SkipLocalsInit]
|
||||
public sealed class Kama : AbstractBase
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly double _fastAlpha;
|
||||
private readonly double _slowAlpha;
|
||||
private readonly RingBuffer _buffer;
|
||||
@@ -50,7 +49,6 @@ public sealed class Kama : AbstractBase
|
||||
if (fastPeriod >= slowPeriod)
|
||||
throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod));
|
||||
|
||||
_period = period;
|
||||
// Buffer needs to hold period + 1 values to calculate Change over 'period' bars
|
||||
// Change = Price[0] - Price[period]
|
||||
_buffer = new RingBuffer(period + 1);
|
||||
@@ -198,23 +196,12 @@ public sealed class Kama : AbstractBase
|
||||
|
||||
source.Times.CopyTo(tSpan);
|
||||
|
||||
// Use static Calculate for performance
|
||||
// fastPeriod = 2/fastAlpha - 1.
|
||||
int fastPeriod = (int)Math.Round(2.0 / _fastAlpha - 1);
|
||||
int slowPeriod = (int)Math.Round(2.0 / _slowAlpha - 1);
|
||||
|
||||
Calculate(source.Values, vSpan, _period, fastPeriod, slowPeriod);
|
||||
|
||||
// Restore state by replaying the entire series
|
||||
// This is expensive but necessary to sync the object state correctly
|
||||
// because KAMA is recursive (IIR) and depends on the full history.
|
||||
Reset();
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Update(new TValue(source.Times[i], source.Values[i]));
|
||||
vSpan[i] = Update(new TValue(source.Times[i], source.Values[i])).Value;
|
||||
}
|
||||
|
||||
Last = new TValue(tSpan[len - 1], _state.Kama);
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
|
||||
+9
-20
@@ -106,21 +106,12 @@ public sealed class Lsma : AbstractBase
|
||||
}
|
||||
else
|
||||
{
|
||||
_buffer.Add(val);
|
||||
_state.SumY += val;
|
||||
|
||||
// Recalculate sum_xy from scratch during warmup
|
||||
_state.SumXY = 0;
|
||||
var span = _buffer.GetSpan();
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
if (_buffer.Count > 0)
|
||||
{
|
||||
// x=0 is newest (index count-1), x=count-1 is oldest (index 0)
|
||||
// buffer stores chronological: [oldest, ..., newest]
|
||||
// index j in buffer corresponds to x = count - 1 - j
|
||||
// sum_xy = sum(x * y)
|
||||
int x = span.Length - 1 - i;
|
||||
_state.SumXY = Math.FusedMultiplyAdd(x, span[i], _state.SumXY);
|
||||
_state.SumXY += _state.SumY;
|
||||
}
|
||||
_state.SumY += val;
|
||||
_buffer.Add(val);
|
||||
}
|
||||
|
||||
_tickCount++;
|
||||
@@ -326,17 +317,15 @@ public sealed class Lsma : AbstractBase
|
||||
{
|
||||
// Warmup phase
|
||||
buffer[count] = val;
|
||||
sum_y += val;
|
||||
count++;
|
||||
|
||||
// Recalculate sum_xy for current count
|
||||
sum_xy = 0;
|
||||
for (int j = 0; j < count; j++)
|
||||
// O(1) update: adding new value at x=0, existing values shift x+1
|
||||
// New value at x=0 contributes 0, existing sum shifts by sum_y
|
||||
if (count > 1)
|
||||
{
|
||||
// buffer[j] is at index j
|
||||
// x = count - 1 - j
|
||||
sum_xy = Math.FusedMultiplyAdd(count - 1 - j, buffer[j], sum_xy);
|
||||
sum_xy += sum_y; // Shift existing values before adding new
|
||||
}
|
||||
sum_y += val;
|
||||
|
||||
if (count <= 1)
|
||||
{
|
||||
|
||||
@@ -47,7 +47,7 @@ public sealed class Pwma : AbstractBase
|
||||
if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
|
||||
_period = period;
|
||||
_divisor = (double)period * (period + 1) * (2 * period + 1) / 6.0;
|
||||
_divisor = (double)period * ((double)period + 1.0) * (2.0 * (double)period + 1.0) / 6.0;
|
||||
_buffer = new RingBuffer(period);
|
||||
_p_buffer = new RingBuffer(period);
|
||||
Name = $"Pwma({period})";
|
||||
@@ -149,7 +149,8 @@ public sealed class Pwma : AbstractBase
|
||||
_buffer.UpdateNewest(val);
|
||||
}
|
||||
|
||||
double currentDivisor = _buffer.IsFull ? _divisor : (double)_buffer.Count * (_buffer.Count + 1) * (2 * _buffer.Count + 1) / 6.0;
|
||||
double count = _buffer.Count;
|
||||
double currentDivisor = _buffer.IsFull ? _divisor : count * (count + 1.0) * (2.0 * count + 1.0) / 6.0;
|
||||
Last = new TValue(input.Time, _state.PSum / currentDivisor);
|
||||
PubEvent(Last);
|
||||
return Last;
|
||||
@@ -244,7 +245,7 @@ public sealed class Pwma : AbstractBase
|
||||
private static void CalculateScalarCore(ReadOnlySpan<double> source, Span<double> output, int period)
|
||||
{
|
||||
int len = source.Length;
|
||||
double divisor = (double)period * (period + 1) * (2 * period + 1) / 6.0;
|
||||
double divisor = (double)period * ((double)period + 1.0) * (2.0 * (double)period + 1.0) / 6.0;
|
||||
double sum = 0;
|
||||
double wsum = 0;
|
||||
double psum = 0;
|
||||
@@ -268,7 +269,7 @@ public sealed class Pwma : AbstractBase
|
||||
psum = Math.FusedMultiplyAdd((double)(i + 1) * (i + 1), val, psum);
|
||||
buffer[i] = val;
|
||||
|
||||
double currentDivisor = (double)(i + 1) * (i + 2) * (2 * (i + 1) + 1) / 6.0;
|
||||
double currentDivisor = ((double)i + 1.0) * ((double)i + 2.0) * (2.0 * ((double)i + 1.0) + 1.0) / 6.0;
|
||||
output[i] = psum / currentDivisor;
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ public sealed class Rma : AbstractBase
|
||||
/// Creates RMA with specified source and period.
|
||||
/// </summary>
|
||||
/// <param name="source">Source series</param>
|
||||
/// <param name="period">Period for RMA calculation (must be > 0)</param>
|
||||
public Rma(TSeries source, int period) : this(period)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
|
||||
@@ -25,11 +25,13 @@ namespace QuanTAlib;
|
||||
/// Becomes true when both internal SMAs are hot.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Trima : AbstractBase
|
||||
public sealed class Trima : AbstractBase, IDisposable
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly Sma _sma1;
|
||||
private readonly Sma _sma2;
|
||||
private readonly Action<TValue> _updateHandler;
|
||||
private ITValuePublisher? _publisher;
|
||||
|
||||
public Trima(int period)
|
||||
{
|
||||
@@ -41,6 +43,7 @@ public sealed class Trima : AbstractBase
|
||||
|
||||
_sma1 = new Sma(p1);
|
||||
_sma2 = new Sma(p2);
|
||||
_updateHandler = (item) => Update(item);
|
||||
|
||||
Name = $"Trima({period})";
|
||||
WarmupPeriod = p1 + p2 - 1;
|
||||
@@ -48,7 +51,17 @@ public sealed class Trima : AbstractBase
|
||||
|
||||
public Trima(ITValuePublisher source, int period) : this(period)
|
||||
{
|
||||
source.Pub += (item) => Update(item);
|
||||
_publisher = source;
|
||||
_publisher.Pub += _updateHandler;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_publisher != null)
|
||||
{
|
||||
_publisher.Pub -= _updateHandler;
|
||||
_publisher = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsHot => _sma1.IsHot && _sma2.IsHot;
|
||||
|
||||
@@ -26,12 +26,14 @@ namespace QuanTAlib;
|
||||
/// - Reacts quickly in trending markets (high volatility)
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Vidya : AbstractBase
|
||||
public sealed class Vidya : AbstractBase, IDisposable
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly double _alpha;
|
||||
private readonly RingBuffer _ups;
|
||||
private readonly RingBuffer _downs;
|
||||
private readonly ITValuePublisher? _source;
|
||||
private readonly Action<TValue>? _pubHandler;
|
||||
|
||||
private record struct State(
|
||||
double PrevClose, double LastVidya,
|
||||
@@ -56,7 +58,17 @@ public sealed class Vidya : AbstractBase
|
||||
|
||||
public Vidya(ITValuePublisher source, int period) : this(period)
|
||||
{
|
||||
source.Pub += (item) => Update(item);
|
||||
_source = source;
|
||||
_pubHandler = (item) => Update(item);
|
||||
_source.Pub += _pubHandler;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_source != null && _pubHandler != null)
|
||||
{
|
||||
_source.Pub -= _pubHandler;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsHot => _state.BarCount >= _period;
|
||||
@@ -145,7 +157,7 @@ public sealed class Vidya : AbstractBase
|
||||
// Replay only the last _period bars to restore internal state
|
||||
Reset();
|
||||
int start = 0;
|
||||
if (len > _period)
|
||||
if (len > 2 * _period)
|
||||
{
|
||||
start = len - _period;
|
||||
_state.BarCount = start;
|
||||
@@ -154,6 +166,18 @@ public sealed class Vidya : AbstractBase
|
||||
_state.LastVidya = vSpan[start - 1];
|
||||
_state.CurrentClose = _state.PrevClose;
|
||||
_state.CurrentVidya = _state.LastVidya;
|
||||
|
||||
// Pre-fill buffers with the previous period's data to ensure correct VI calculation
|
||||
for (int i = start - _period; i < start; i++)
|
||||
{
|
||||
double price = source.Values[i];
|
||||
double prev = source.Values[i - 1];
|
||||
double change = price - prev;
|
||||
double up = change > 0 ? change : 0;
|
||||
double down = change < 0 ? -change : 0;
|
||||
_ups.Add(up);
|
||||
_downs.Add(down);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = start; i < len; i++)
|
||||
|
||||
@@ -189,4 +189,61 @@ public class WmaTests
|
||||
// Last value should remain unchanged if unsubscribed
|
||||
Assert.Equal(100, wma.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultLastValidValue_IsNaN()
|
||||
{
|
||||
var wma = new Wma(10);
|
||||
Assert.True(double.IsNaN(wma.DefaultLastValidValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InitialNaNs_ResultInNaN()
|
||||
{
|
||||
var wma = new Wma(5);
|
||||
wma.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
Assert.True(double.IsNaN(wma.Last.Value));
|
||||
|
||||
wma.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
Assert.True(double.IsNaN(wma.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecoveryFromNaN_Works()
|
||||
{
|
||||
var wma = new Wma(3);
|
||||
|
||||
// Feed NaNs
|
||||
wma.Update(new TValue(DateTime.UtcNow, double.NaN)); // [NaN]
|
||||
Assert.True(double.IsNaN(wma.Last.Value));
|
||||
|
||||
wma.Update(new TValue(DateTime.UtcNow, double.NaN)); // [NaN, NaN]
|
||||
Assert.True(double.IsNaN(wma.Last.Value));
|
||||
|
||||
// Feed valid values
|
||||
wma.Update(new TValue(DateTime.UtcNow, 1.0)); // [NaN, NaN, 1] -> Sum is NaN
|
||||
Assert.True(double.IsNaN(wma.Last.Value));
|
||||
|
||||
wma.Update(new TValue(DateTime.UtcNow, 2.0)); // [NaN, 1, 2] -> Sum is NaN
|
||||
Assert.True(double.IsNaN(wma.Last.Value));
|
||||
|
||||
wma.Update(new TValue(DateTime.UtcNow, 3.0)); // [1, 2, 3] -> Sum should recover!
|
||||
// WMA(3) of [1, 2, 3] = (1*1 + 2*2 + 3*3) / 6 = (1+4+9)/6 = 14/6 = 2.333...
|
||||
Assert.Equal(2.333333333, wma.Last.Value, 1e-6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfigurableDefault_Works()
|
||||
{
|
||||
var wma = new Wma(3) { DefaultLastValidValue = 0 };
|
||||
|
||||
// Feed NaN
|
||||
wma.Update(new TValue(DateTime.UtcNow, double.NaN)); // Treated as 0 -> [0]
|
||||
// WMA(3) of [0] -> (1*0)/1 = 0
|
||||
Assert.Equal(0, wma.Last.Value);
|
||||
|
||||
wma.Update(new TValue(DateTime.UtcNow, 3.0)); // [0, 3]
|
||||
// WMA(3) of [0, 3] -> (1*0 + 2*3) / 3 = 6/3 = 2
|
||||
Assert.Equal(2, wma.Last.Value);
|
||||
}
|
||||
}
|
||||
|
||||
+19
-6
@@ -35,10 +35,16 @@ public sealed class Wma : AbstractBase, IDisposable
|
||||
private readonly ITValuePublisher? _source;
|
||||
private readonly Action<TValue>? _handler;
|
||||
|
||||
private record struct State(double Sum, double WSum, double LastInput, double LastValidValue, int TickCount);
|
||||
private record struct State(double Sum, double WSum, double LastInput, double LastValidValue, int TickCount, bool HasSeenValidData);
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
|
||||
/// <summary>
|
||||
/// Default value to use for LastValidValue when no valid data has been seen yet.
|
||||
/// Defaults to double.NaN to avoid silently introducing zeros.
|
||||
/// </summary>
|
||||
public double DefaultLastValidValue { get; set; } = double.NaN;
|
||||
|
||||
private const int ResyncInterval = 10000;
|
||||
|
||||
private static readonly Vector512<long> V512_Idx_1 = Vector512.Create(0L, 0, 1, 2, 3, 4, 5, 6);
|
||||
@@ -82,9 +88,10 @@ public sealed class Wma : AbstractBase, IDisposable
|
||||
if (double.IsFinite(input))
|
||||
{
|
||||
_state.LastValidValue = input;
|
||||
_state.HasSeenValidData = true;
|
||||
return input;
|
||||
}
|
||||
return _state.LastValidValue;
|
||||
return _state.HasSeenValidData ? _state.LastValidValue : DefaultLastValidValue;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
@@ -107,7 +114,10 @@ public sealed class Wma : AbstractBase, IDisposable
|
||||
_buffer.Add(val);
|
||||
|
||||
_state.TickCount++;
|
||||
if (_buffer.IsFull && _state.TickCount >= ResyncInterval)
|
||||
bool isNaN = double.IsNaN(_state.Sum) || double.IsNaN(_state.WSum);
|
||||
bool needResync = _buffer.IsFull && _state.TickCount >= ResyncInterval;
|
||||
|
||||
if (needResync || (isNaN && double.IsFinite(val)))
|
||||
{
|
||||
_state.TickCount = 0;
|
||||
double recalcSum = 0;
|
||||
@@ -184,7 +194,8 @@ public sealed class Wma : AbstractBase, IDisposable
|
||||
int startIndex = len - windowSize;
|
||||
|
||||
// Seed LastValidValue
|
||||
_state.LastValidValue = 0;
|
||||
_state.LastValidValue = DefaultLastValidValue;
|
||||
_state.HasSeenValidData = false;
|
||||
if (startIndex > 0)
|
||||
{
|
||||
for (int i = startIndex - 1; i >= 0; i--)
|
||||
@@ -192,6 +203,7 @@ public sealed class Wma : AbstractBase, IDisposable
|
||||
if (double.IsFinite(source[i]))
|
||||
{
|
||||
_state.LastValidValue = source[i];
|
||||
_state.HasSeenValidData = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -272,7 +284,7 @@ public sealed class Wma : AbstractBase, IDisposable
|
||||
double divisor = (double)period * (period + 1) * 0.5;
|
||||
double sum = 0;
|
||||
double wsum = 0;
|
||||
double lastValid = 0;
|
||||
double lastValid = double.NaN;
|
||||
|
||||
Span<double> buffer = period <= 512 ? stackalloc double[period] : new double[period];
|
||||
int bufferIdx = 0;
|
||||
@@ -317,7 +329,8 @@ public sealed class Wma : AbstractBase, IDisposable
|
||||
output[i] = wsum / divisor;
|
||||
|
||||
tickCount++;
|
||||
if (tickCount >= ResyncInterval)
|
||||
bool isNaN = double.IsNaN(sum) || double.IsNaN(wsum);
|
||||
if (tickCount >= ResyncInterval || (isNaN && double.IsFinite(val)))
|
||||
{
|
||||
tickCount = 0;
|
||||
double recalcSum = 0;
|
||||
|
||||
Reference in New Issue
Block a user