mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 12:38:06 +00:00
feat(validation): add input validation for Bilateral and Blma constructors; enhance Butter and Mgdi calculations with NaN handling
This commit is contained in:
@@ -266,6 +266,9 @@ public sealed class Bilateral : AbstractBase
|
|||||||
if (period <= 0)
|
if (period <= 0)
|
||||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||||
|
|
||||||
|
if (destination.Length < source.Length)
|
||||||
|
throw new ArgumentException("Destination must have length >= source length", nameof(destination));
|
||||||
|
|
||||||
// Precalculate spatial weights
|
// Precalculate spatial weights
|
||||||
double sigmaS = Math.Max(period * sigmaSRatio, 1e-10);
|
double sigmaS = Math.Max(period * sigmaSRatio, 1e-10);
|
||||||
double twoSigmaSSq = 2.0 * sigmaS * sigmaS;
|
double twoSigmaSSq = 2.0 * sigmaS * sigmaS;
|
||||||
@@ -304,7 +307,7 @@ public sealed class Bilateral : AbstractBase
|
|||||||
for (int i = 0; i < source.Length; i++)
|
for (int i = 0; i < source.Length; i++)
|
||||||
{
|
{
|
||||||
double val = source[i];
|
double val = source[i];
|
||||||
if (double.IsNaN(val))
|
if (!double.IsFinite(val))
|
||||||
{
|
{
|
||||||
val = lastValid;
|
val = lastValid;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,22 +22,16 @@ public class BlmaTests
|
|||||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Blma(-1));
|
Assert.Throws<ArgumentOutOfRangeException>(() => new Blma(-1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_ValidatesSource()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => new Blma(null!, 10));
|
||||||
|
Assert.Throws<ArgumentException>(() => new Blma(new object(), 10));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void BasicCalculation_MatchesManual()
|
public void BasicCalculation_MatchesManual()
|
||||||
{
|
{
|
||||||
// Period 3
|
|
||||||
// Weights:
|
|
||||||
// n=3
|
|
||||||
// i=0: 0.42 - 0.5*cos(0) + 0.08*cos(0) = 0.42 - 0.5 + 0.08 = 0
|
|
||||||
// i=1: 0.42 - 0.5*cos(pi) + 0.08*cos(2pi) = 0.42 - 0.5(-1) + 0.08(1) = 0.42 + 0.5 + 0.08 = 1.0
|
|
||||||
// i=2: 0.42 - 0.5*cos(2pi) + 0.08*cos(4pi) = 0.42 - 0.5(1) + 0.08(1) = 0
|
|
||||||
// Wait, Blackman window is 0 at edges.
|
|
||||||
// So for period 3, weights are [0, 1, 0].
|
|
||||||
// Sum = 1.
|
|
||||||
// Weighted Sum = 0*x0 + 1*x1 + 0*x2 = x1.
|
|
||||||
// So BLMA(3) should return the middle value?
|
|
||||||
// Let's verify.
|
|
||||||
|
|
||||||
var blma = new Blma(3);
|
var blma = new Blma(3);
|
||||||
var input = new[] { 10.0, 20.0, 30.0 };
|
var input = new[] { 10.0, 20.0, 30.0 };
|
||||||
|
|
||||||
@@ -45,44 +39,12 @@ public class BlmaTests
|
|||||||
var r1 = blma.Update(new TValue(DateTime.UtcNow, input[0]));
|
var r1 = blma.Update(new TValue(DateTime.UtcNow, input[0]));
|
||||||
Assert.Equal(10.0, r1.Value);
|
Assert.Equal(10.0, r1.Value);
|
||||||
|
|
||||||
// Bar 2: Count=2. Weights for n=2:
|
// Bar 2: Count=2. Weights for n=2 sum to 0. Fallback to average: (10+20)/2 = 15.
|
||||||
// i=0: 0.42 - 0.5*cos(0) + 0.08*cos(0) = 0
|
|
||||||
// i=1: 0.42 - 0.5*cos(2pi) + 0.08*cos(4pi) = 0
|
|
||||||
// Wait, for n=2, invNMinus1 = 1/(2-1) = 1.
|
|
||||||
// i=0: ratio=0. w=0.
|
|
||||||
// i=1: ratio=1. w=0.
|
|
||||||
// Sum=0. Division by zero?
|
|
||||||
// Let's check CalculateWeights logic.
|
|
||||||
// If n=2, weights are 0, 0. Sum is 0.
|
|
||||||
// This is a known issue with Blackman window for small N if we strictly follow formula.
|
|
||||||
// However, usually N is odd or larger.
|
|
||||||
// But for warmup, we encounter N=2.
|
|
||||||
// If sum is 0, result is NaN or Infinity.
|
|
||||||
// We should check if sum is 0 and handle it?
|
|
||||||
// Or maybe the formula handles it?
|
|
||||||
// Let's check the code.
|
|
||||||
// If sum is 0, we divide by 0.
|
|
||||||
// I should add a check in CalculateWeights or Update to handle zero sum?
|
|
||||||
// Or maybe for N=2, we should use something else?
|
|
||||||
// PineScript implementation:
|
|
||||||
// If total_weight is 0, inv_total is Infinity.
|
|
||||||
// Then weights become Infinity.
|
|
||||||
// Then result is Infinity.
|
|
||||||
// Does PineScript handle this?
|
|
||||||
// "int p = math.min(bar_index + 1, period)"
|
|
||||||
// If period=2, p=2.
|
|
||||||
// If Blackman gives 0 weights, it fails.
|
|
||||||
// But maybe `cos(2pi)` is not exactly 1 in float?
|
|
||||||
// No, it's mathematically 0.
|
|
||||||
// Let's see if I need to fix this in Blma.cs.
|
|
||||||
// I will run this test and see if it fails.
|
|
||||||
|
|
||||||
var r2 = blma.Update(new TValue(DateTime.UtcNow, input[1]));
|
var r2 = blma.Update(new TValue(DateTime.UtcNow, input[1]));
|
||||||
// For N=2, weights sum to 0. Fallback to average: (10+20)/2 = 15.
|
|
||||||
Assert.Equal(15.0, r2.Value);
|
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]));
|
var r3 = blma.Update(new TValue(DateTime.UtcNow, input[2]));
|
||||||
// For N=3, weights [0, 1, 0]. Sum=1. Result=20.
|
|
||||||
Assert.Equal(20.0, r3.Value, 1e-6);
|
Assert.Equal(20.0, r3.Value, 1e-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,9 +118,6 @@ public class BlmaTests
|
|||||||
Assert.Equal(val1, val2);
|
Assert.Equal(val1, val2);
|
||||||
|
|
||||||
// However, the internal buffer MUST be updated.
|
// However, the internal buffer MUST be updated.
|
||||||
// We verify this by adding a 4th bar.
|
|
||||||
// If Bar 3 was 30, Bar 4 result would be different than if Bar 3 is 40.
|
|
||||||
|
|
||||||
// Case A: Bar 3 = 40 (current state)
|
// Case A: Bar 3 = 40 (current state)
|
||||||
blma.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
|
blma.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
|
||||||
var valWith40 = blma.Last.Value;
|
var valWith40 = blma.Last.Value;
|
||||||
@@ -173,4 +132,46 @@ public class BlmaTests
|
|||||||
|
|
||||||
Assert.NotEqual(valWith30, valWith40);
|
Assert.NotEqual(valWith30, valWith40);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Prime_PreservesTimestamps()
|
||||||
|
{
|
||||||
|
var blma = new Blma(5);
|
||||||
|
var input = new double[] { 1, 2, 3, 4, 5 };
|
||||||
|
var timestamps = new List<DateTime>();
|
||||||
|
|
||||||
|
blma.Pub += (item) => timestamps.Add(item.AsDateTime);
|
||||||
|
|
||||||
|
blma.Prime(input);
|
||||||
|
|
||||||
|
Assert.Equal(input.Length, timestamps.Count);
|
||||||
|
// Verify timestamps are unique and increasing
|
||||||
|
for (int i = 1; i < timestamps.Count; i++)
|
||||||
|
{
|
||||||
|
Assert.True(timestamps[i] > timestamps[i-1], $"Timestamp at {i} ({timestamps[i].Ticks}) should be greater than {i-1} ({timestamps[i-1].Ticks})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Prime_Overload_UsesProvidedTimestamps()
|
||||||
|
{
|
||||||
|
var blma = new Blma(5);
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
TValue[] input =
|
||||||
|
[
|
||||||
|
new(now, 1),
|
||||||
|
new(now.AddMinutes(1), 2),
|
||||||
|
new(now.AddMinutes(2), 3)
|
||||||
|
];
|
||||||
|
var timestamps = new List<DateTime>();
|
||||||
|
|
||||||
|
blma.Pub += (item) => timestamps.Add(item.AsDateTime);
|
||||||
|
|
||||||
|
blma.Prime(input);
|
||||||
|
|
||||||
|
Assert.Equal(input.Length, timestamps.Count);
|
||||||
|
Assert.Equal(input[0].AsDateTime, timestamps[0]);
|
||||||
|
Assert.Equal(input[1].AsDateTime, timestamps[1]);
|
||||||
|
Assert.Equal(input[2].AsDateTime, timestamps[2]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-3
@@ -33,8 +33,19 @@ public sealed class Blma : AbstractBase
|
|||||||
|
|
||||||
public Blma(object source, int period) : this(period)
|
public Blma(object source, int period) : this(period)
|
||||||
{
|
{
|
||||||
var pub = (ITValuePublisher)source;
|
if (source is null)
|
||||||
pub.Pub += Handle;
|
{
|
||||||
|
throw new ArgumentNullException(nameof(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source is ITValuePublisher pub)
|
||||||
|
{
|
||||||
|
pub.Pub += Handle;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Source must implement ITValuePublisher", nameof(source));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Handle(TValue value)
|
private void Handle(TValue value)
|
||||||
@@ -49,9 +60,19 @@ public sealed class Blma : AbstractBase
|
|||||||
|
|
||||||
public override void Prime(ReadOnlySpan<double> source)
|
public override void Prime(ReadOnlySpan<double> source)
|
||||||
{
|
{
|
||||||
|
DateTime time = DateTime.UtcNow;
|
||||||
foreach (var value in source)
|
foreach (var value in source)
|
||||||
{
|
{
|
||||||
Update(new TValue(DateTime.UtcNow, value));
|
Update(new TValue(time, value));
|
||||||
|
time = time.AddMilliseconds(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Prime(ReadOnlySpan<TValue> source)
|
||||||
|
{
|
||||||
|
foreach (var value in source)
|
||||||
|
{
|
||||||
|
Update(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public class ButterTests
|
|||||||
{
|
{
|
||||||
var source = new double[10];
|
var source = new double[10];
|
||||||
var destination = new double[5];
|
var destination = new double[5];
|
||||||
Assert.Throws<ArgumentOutOfRangeException>(() => Butter.Calculate(source, destination, 5));
|
Assert.Throws<ArgumentOutOfRangeException>(() => Butter.Calculate(source, destination, 5, double.NaN));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -59,6 +59,14 @@ public class ButterTests
|
|||||||
Assert.Equal(100, result.Value);
|
Assert.Equal(100, result.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Initial_NaN_Input_ReturnsNaN()
|
||||||
|
{
|
||||||
|
var butter = new Butter(10);
|
||||||
|
var result = butter.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||||
|
Assert.True(double.IsNaN(result.Value));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AllModes_ProduceSameResult()
|
public void AllModes_ProduceSameResult()
|
||||||
{
|
{
|
||||||
@@ -74,7 +82,7 @@ public class ButterTests
|
|||||||
var tValues = series.Values.ToArray();
|
var tValues = series.Values.ToArray();
|
||||||
var spanInput = new ReadOnlySpan<double>(tValues);
|
var spanInput = new ReadOnlySpan<double>(tValues);
|
||||||
var spanOutput = new double[tValues.Length];
|
var spanOutput = new double[tValues.Length];
|
||||||
Butter.Calculate(spanInput, spanOutput, period);
|
Butter.Calculate(spanInput, spanOutput, period, double.NaN);
|
||||||
double spanResult = spanOutput[^1];
|
double spanResult = spanOutput[^1];
|
||||||
|
|
||||||
// 3. Streaming Mode
|
// 3. Streaming Mode
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ public sealed class Butter : AbstractBase
|
|||||||
{
|
{
|
||||||
_state = new State();
|
_state = new State();
|
||||||
_p_state = new State();
|
_p_state = new State();
|
||||||
|
Last = new TValue(0, double.NaN);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Reset()
|
public override void Reset()
|
||||||
@@ -97,6 +98,7 @@ public sealed class Butter : AbstractBase
|
|||||||
|
|
||||||
if (double.IsNaN(input.Value) || double.IsInfinity(input.Value))
|
if (double.IsNaN(input.Value) || double.IsInfinity(input.Value))
|
||||||
{
|
{
|
||||||
|
// Return Last (initialized to NaN) if no valid input has been seen yet
|
||||||
return Last;
|
return Last;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +128,7 @@ public sealed class Butter : AbstractBase
|
|||||||
{
|
{
|
||||||
var result = new TSeries();
|
var result = new TSeries();
|
||||||
Span<double> output = new double[source.Count];
|
Span<double> output = new double[source.Count];
|
||||||
Calculate(source.Values, output, _period);
|
Calculate(source.Values, output, _period, double.NaN);
|
||||||
|
|
||||||
for (int i = 0; i < source.Count; i++)
|
for (int i = 0; i < source.Count; i++)
|
||||||
{
|
{
|
||||||
@@ -148,7 +150,7 @@ public sealed class Butter : AbstractBase
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> destination, int period)
|
public static void Calculate(ReadOnlySpan<double> source, Span<double> destination, int period, double initialLast)
|
||||||
{
|
{
|
||||||
if (period < 2)
|
if (period < 2)
|
||||||
{
|
{
|
||||||
@@ -183,7 +185,7 @@ public sealed class Butter : AbstractBase
|
|||||||
double x = source[i];
|
double x = source[i];
|
||||||
if (double.IsNaN(x) || double.IsInfinity(x))
|
if (double.IsNaN(x) || double.IsInfinity(x))
|
||||||
{
|
{
|
||||||
destination[i] = i > 0 ? destination[i - 1] : 0;
|
destination[i] = i > 0 ? destination[i - 1] : initialLast;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
double y = i < 2
|
double y = i < 2
|
||||||
|
|||||||
@@ -551,6 +551,17 @@ public class EmaTests
|
|||||||
Assert.Equal(verifyEma.Last.Value, ema.Last.Value, 1e-10);
|
Assert.Equal(verifyEma.Last.Value, ema.Last.Value, 1e-10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Prime_AllNaNs_ReturnsNaN()
|
||||||
|
{
|
||||||
|
var ema = new Ema(5);
|
||||||
|
double[] history = [double.NaN, double.NaN, double.NaN];
|
||||||
|
|
||||||
|
ema.Prime(history);
|
||||||
|
|
||||||
|
Assert.True(double.IsNaN(ema.Last.Value));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Calculate_ReturnsCorrectResultsAndHotIndicator()
|
public void Calculate_ReturnsCorrectResultsAndHotIndicator()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -118,15 +118,25 @@ public sealed class Ema : AbstractBase
|
|||||||
int i = 0;
|
int i = 0;
|
||||||
|
|
||||||
// Find first valid value to seed lastValid
|
// Find first valid value to seed lastValid
|
||||||
|
bool foundValid = false;
|
||||||
for (int k = 0; k < len; k++)
|
for (int k = 0; k < len; k++)
|
||||||
{
|
{
|
||||||
if (double.IsFinite(source[k]))
|
if (double.IsFinite(source[k]))
|
||||||
{
|
{
|
||||||
_lastValidValue = source[k];
|
_lastValidValue = source[k];
|
||||||
|
foundValid = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!foundValid)
|
||||||
|
{
|
||||||
|
Last = new TValue(DateTime.MinValue, double.NaN);
|
||||||
|
_p_state = _state;
|
||||||
|
_p_lastValidValue = _lastValidValue;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!_state.IsCompensated)
|
if (!_state.IsCompensated)
|
||||||
{
|
{
|
||||||
for (; i < len && _state.E > COMPENSATOR_THRESHOLD; i++)
|
for (; i < len && _state.E > COMPENSATOR_THRESHOLD; i++)
|
||||||
|
|||||||
@@ -237,6 +237,25 @@ public class JmaTests
|
|||||||
Assert.NotEqual(jmaPhase0.Last.Value, jmaPhaseMinus100.Last.Value);
|
Assert.NotEqual(jmaPhase0.Last.Value, jmaPhaseMinus100.Last.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Jma_Power_AffectsResult()
|
||||||
|
{
|
||||||
|
var series = new TSeries();
|
||||||
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||||
|
for (int i = 0; i < 100; i++)
|
||||||
|
{
|
||||||
|
var bar = gbm.Next(isNew: true);
|
||||||
|
series.Add(bar.Time, bar.Close);
|
||||||
|
}
|
||||||
|
|
||||||
|
var jmaPowerDefault = Jma.Batch(series, 10, power: 0.45);
|
||||||
|
var jmaPower1 = Jma.Batch(series, 10, power: 1.0);
|
||||||
|
var jmaPower2 = Jma.Batch(series, 10, power: 2.0);
|
||||||
|
|
||||||
|
Assert.NotEqual(jmaPowerDefault.Last.Value, jmaPower1.Last.Value);
|
||||||
|
Assert.NotEqual(jmaPowerDefault.Last.Value, jmaPower2.Last.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Jma_SpanCalc_HandlesNaN()
|
public void Jma_SpanCalc_HandlesNaN()
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public sealed class Jma : AbstractBase
|
|||||||
private readonly double _lengthDivider; // L'/(L'+2), L' = 0.9*L
|
private readonly double _lengthDivider; // L'/(L'+2), L' = 0.9*L
|
||||||
private readonly double _logSqrtDivider; // Precomputed log(_sqrtDivider) for Exp optimization
|
private readonly double _logSqrtDivider; // Precomputed log(_sqrtDivider) for Exp optimization
|
||||||
private readonly double _logLengthDivider; // Precomputed log(_lengthDivider) for Exp optimization
|
private readonly double _logLengthDivider; // Precomputed log(_lengthDivider) for Exp optimization
|
||||||
|
private readonly double _power; // Jurik power parameter
|
||||||
|
|
||||||
// Constants for trimmed mean
|
// Constants for trimmed mean
|
||||||
private const int JurikTrimCount = 65; // canonical JMA: middle 65 of 128 samples
|
private const int JurikTrimCount = 65; // canonical JMA: middle 65 of 128 samples
|
||||||
@@ -71,6 +72,8 @@ public sealed class Jma : AbstractBase
|
|||||||
else
|
else
|
||||||
_phaseParam = (phase * 0.01) + 1.5;
|
_phaseParam = (phase * 0.01) + 1.5;
|
||||||
|
|
||||||
|
_power = power;
|
||||||
|
|
||||||
// --- Length / log / divider parameters (from decompiled JMA) ---
|
// --- Length / log / divider parameters (from decompiled JMA) ---
|
||||||
// L_raw ~ (period - 1)/2, with a tiny lower bound to avoid log(0)
|
// L_raw ~ (period - 1)/2, with a tiny lower bound to avoid log(0)
|
||||||
double lengthParam = period < 1.0000000002
|
double lengthParam = period < 1.0000000002
|
||||||
@@ -144,7 +147,11 @@ public sealed class Jma : AbstractBase
|
|||||||
// --- Handle NaN/inf: reuse last finite price ---
|
// --- Handle NaN/inf: reuse last finite price ---
|
||||||
if (!double.IsFinite(value))
|
if (!double.IsFinite(value))
|
||||||
{
|
{
|
||||||
value = _state.Bars > 0 ? _state.LastPrice : 0.0;
|
if (_state.Bars == 0)
|
||||||
|
{
|
||||||
|
return double.NaN;
|
||||||
|
}
|
||||||
|
value = _state.LastPrice;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -189,8 +196,7 @@ public sealed class Jma : AbstractBase
|
|||||||
double ratio = absValue / refVolatility;
|
double ratio = absValue / refVolatility;
|
||||||
if (ratio < 0.0) ratio = 0.0;
|
if (ratio < 0.0) ratio = 0.0;
|
||||||
|
|
||||||
double p = Math.Max(_logParam - 2.0, 0.5);
|
double d = Math.Pow(ratio, _power);
|
||||||
double d = Math.Pow(ratio, p);
|
|
||||||
if (d > _logParam) d = _logParam;
|
if (d > _logParam) d = _logParam;
|
||||||
if (d < 1.0) d = 1.0;
|
if (d < 1.0) d = 1.0;
|
||||||
|
|
||||||
|
|||||||
@@ -268,6 +268,10 @@ public sealed class Mama : AbstractBase
|
|||||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, double fastLimit = 0.5, double slowLimit = 0.05)
|
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, double fastLimit = 0.5, double slowLimit = 0.05)
|
||||||
{
|
{
|
||||||
if (source.Length == 0) return;
|
if (source.Length == 0) return;
|
||||||
|
if (output.Length < source.Length)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(output), "Output buffer must be at least as large as the input buffer.");
|
||||||
|
}
|
||||||
|
|
||||||
// Stack allocate buffers for high performance (size 8 for power of 2 masking)
|
// Stack allocate buffers for high performance (size 8 for power of 2 masking)
|
||||||
// We need 7 elements, but 8 allows & 7 masking
|
// We need 7 elements, but 8 allows & 7 masking
|
||||||
|
|||||||
@@ -44,4 +44,17 @@ public class MgdiTests
|
|||||||
Assert.True(result.Value > 100.0);
|
Assert.True(result.Value > 100.0);
|
||||||
Assert.True(result.Value < 101.0);
|
Assert.True(result.Value < 101.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Calculate_InvalidK_ThrowsArgumentOutOfRangeException()
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => Mgdi.Calculate(source, output, 14, 0));
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => Mgdi.Calculate(source, output, 14, -1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ public sealed class Mgdi : AbstractBase
|
|||||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period = 14, double k = 0.6)
|
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period = 14, double k = 0.6)
|
||||||
{
|
{
|
||||||
ArgumentOutOfRangeException.ThrowIfLessThan(period, 1);
|
ArgumentOutOfRangeException.ThrowIfLessThan(period, 1);
|
||||||
if (k <= 0) throw new ArgumentOutOfRangeException(nameof(k), "k must be greater than 0");
|
if (double.IsNaN(k) || double.IsInfinity(k) || k <= 0) throw new ArgumentOutOfRangeException(nameof(k), "k must be a finite value greater than 0");
|
||||||
|
|
||||||
if (source.Length != output.Length)
|
if (source.Length != output.Length)
|
||||||
throw new ArgumentException("Source and output must have the same length");
|
throw new ArgumentException("Source and output must have the same length");
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public sealed class Usf : AbstractBase
|
|||||||
{
|
{
|
||||||
private record struct State(double Usf1, double Usf2, double PrevInput1, double PrevInput2, double LastValidValue, int Count, bool IsHot)
|
private record struct State(double Usf1, double Usf2, double PrevInput1, double PrevInput2, double LastValidValue, int Count, bool IsHot)
|
||||||
{
|
{
|
||||||
public static State New() => new() { Usf1 = 0, Usf2 = 0, PrevInput1 = 0, PrevInput2 = 0, LastValidValue = 0, Count = 0, IsHot = false };
|
public static State New() => new() { Usf1 = 0, Usf2 = 0, PrevInput1 = 0, PrevInput2 = 0, LastValidValue = double.NaN, Count = 0, IsHot = false };
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly double _c1, _c2, _c3;
|
private readonly double _c1, _c2, _c3;
|
||||||
|
|||||||
@@ -170,4 +170,23 @@ public class WmaTests
|
|||||||
Assert.Throws<ArgumentException>(() => new Wma(0));
|
Assert.Throws<ArgumentException>(() => new Wma(0));
|
||||||
Assert.Throws<ArgumentException>(() => new Wma(-1));
|
Assert.Throws<ArgumentException>(() => new Wma(-1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Dispose_UnsubscribesFromSource()
|
||||||
|
{
|
||||||
|
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
|
||||||
|
Assert.Equal(100, wma.Last.Value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-2
@@ -27,11 +27,13 @@ namespace QuanTAlib;
|
|||||||
/// Becomes true when the buffer is full (period samples processed).
|
/// Becomes true when the buffer is full (period samples processed).
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[SkipLocalsInit]
|
[SkipLocalsInit]
|
||||||
public sealed class Wma : AbstractBase
|
public sealed class Wma : AbstractBase, IDisposable
|
||||||
{
|
{
|
||||||
private readonly int _period;
|
private readonly int _period;
|
||||||
private readonly double _divisor;
|
private readonly double _divisor;
|
||||||
private readonly RingBuffer _buffer;
|
private readonly RingBuffer _buffer;
|
||||||
|
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);
|
||||||
private State _state;
|
private State _state;
|
||||||
@@ -59,7 +61,17 @@ public sealed class Wma : AbstractBase
|
|||||||
|
|
||||||
public Wma(ITValuePublisher source, int period) : this(period)
|
public Wma(ITValuePublisher source, int period) : this(period)
|
||||||
{
|
{
|
||||||
source.Pub += (item) => Update(item);
|
_source = source;
|
||||||
|
_handler = (item) => Update(item);
|
||||||
|
_source.Pub += _handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_source != null && _handler != null)
|
||||||
|
{
|
||||||
|
_source.Pub -= _handler;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool IsHot => _buffer.IsFull;
|
public override bool IsHot => _buffer.IsFull;
|
||||||
|
|||||||
Reference in New Issue
Block a user