mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-09 14:30:56 +00:00
Add Kahan-Babuška Summation Algorithm and Enhance Variance Indicator Tests
- Introduced a new `Sum` class implementing the Kahan-Babuška algorithm for high-precision rolling summation. - Added comprehensive documentation for the `Sum` class, detailing its mathematical foundation, performance profile, and use cases. - Refactored `VarianceIndicator` tests to improve clarity and coverage, including checks for different source types and the ability to change properties. - Enhanced `UsfIndicator` tests to validate initialization, processing of updates, and property changes. - Updated `UsfIndicator` implementation to simplify source handling and improve short name generation. - Modified Qodana configuration to exclude unused auto property accessor warnings.
This commit is contained in:
+2
-1
@@ -74,6 +74,7 @@
|
||||
- [LINREG - Linear Regression Curve](../lib/statistics/linreg/LinReg.md)
|
||||
- [MEDIAN - Rolling Median](../lib/statistics/median/Median.md)
|
||||
- [SKEW - Skewness](../lib/statistics/skew/Skew.md)
|
||||
- [SUM - Rolling Sum](../lib/statistics/sum/Sum.md)
|
||||
- [VARIANCE - Population and Sample Variance](../lib/statistics/variance/Variance.md)
|
||||
|
||||
- **Numerics**
|
||||
@@ -86,4 +87,4 @@
|
||||
- [Overview](../lib/forecasts/_index.md)
|
||||
|
||||
- **Cycles**
|
||||
- [Overview](../lib/cycles/_index.md)
|
||||
- [Overview](../lib/cycles/_index.md)
|
||||
|
||||
+2
-1
@@ -110,4 +110,5 @@ These measure the spread of data points around the mean.
|
||||
- [**MEDIAN**](../lib/statistics/median/Median.md) - Rolling Median
|
||||
- [**SKEW**](../lib/statistics/skew/Skew.md) - Skewness
|
||||
- [**STDDEV**](../lib/statistics/stddev/StdDev.md) - Standard Deviation
|
||||
- [**VARIANCE**](../lib/statistics/variance/Variance.md) - Population and Sample Variance
|
||||
- [**SUM**](../lib/statistics/sum/Sum.md) - Rolling Sum
|
||||
- [**VARIANCE**](../lib/statistics/variance/Variance.md) - Population and Sample Variance
|
||||
|
||||
+2
-1
@@ -283,4 +283,5 @@
|
||||
| **Median (Statistical)** | [Median](../lib/statistics/median/Median.md) | ✔️ | - | - | - |
|
||||
| **Skewness** | [Skew](../lib/statistics/skew/Skew.md) | ✔️ | - | - | - |
|
||||
| **Standard Deviation** | [StdDev](../lib/statistics/stddev/StdDev.md) | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| **Variance** | [Variance](../lib/statistics/variance/Variance.md) | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| **Sum (Rolling)** | [Sum](../lib/statistics/sum/Sum.md) | - | ✔️ | ✔️ | - |
|
||||
| **Variance** | [Variance](../lib/statistics/variance/Variance.md) | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
|
||||
@@ -11,12 +11,12 @@ public abstract class AbstractBase : ITValuePublisher
|
||||
/// <summary>
|
||||
/// Display name for the indicator.
|
||||
/// </summary>
|
||||
public string Name { get; protected set; } = string.Empty;
|
||||
public string Name { get; protected init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Number of periods before the indicator is considered "hot" (valid).
|
||||
/// </summary>
|
||||
public int WarmupPeriod { get; protected set; }
|
||||
public int WarmupPeriod { get; protected init; }
|
||||
|
||||
/// <summary>
|
||||
/// Current value of the indicator.
|
||||
|
||||
@@ -0,0 +1,541 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TValueEventArgsTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_WithValueAndIsNew_SetsPropertiesCorrectly()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, 123.45);
|
||||
bool isNew = true;
|
||||
|
||||
var eventArgs = new TValueEventArgs { Value = tValue, IsNew = isNew };
|
||||
|
||||
Assert.Equal(tValue, eventArgs.Value);
|
||||
Assert.True(eventArgs.IsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Default_SetsDefaultValues()
|
||||
{
|
||||
var eventArgs = new TValueEventArgs();
|
||||
|
||||
Assert.Equal(default(TValue), eventArgs.Value);
|
||||
Assert.False(eventArgs.IsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNaNValue_PreservesNaN()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, double.NaN);
|
||||
|
||||
var eventArgs = new TValueEventArgs { Value = tValue, IsNew = false };
|
||||
|
||||
Assert.True(double.IsNaN(eventArgs.Value.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithInfinityValue_PreservesInfinity()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, double.PositiveInfinity);
|
||||
|
||||
var eventArgs = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
|
||||
Assert.True(double.IsPositiveInfinity(eventArgs.Value.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_SameValues_ReturnsTrue()
|
||||
{
|
||||
var tValue = new TValue(12345, 100.0);
|
||||
var args1 = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
var args2 = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
|
||||
Assert.True(args1.Equals(args2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_DifferentValue_ReturnsFalse()
|
||||
{
|
||||
var tValue1 = new TValue(12345, 100.0);
|
||||
var tValue2 = new TValue(12345, 101.0);
|
||||
var args1 = new TValueEventArgs { Value = tValue1, IsNew = true };
|
||||
var args2 = new TValueEventArgs { Value = tValue2, IsNew = true };
|
||||
|
||||
Assert.False(args1.Equals(args2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_DifferentTime_ReturnsFalse()
|
||||
{
|
||||
var tValue1 = new TValue(12345, 100.0);
|
||||
var tValue2 = new TValue(12346, 100.0);
|
||||
var args1 = new TValueEventArgs { Value = tValue1, IsNew = true };
|
||||
var args2 = new TValueEventArgs { Value = tValue2, IsNew = true };
|
||||
|
||||
Assert.False(args1.Equals(args2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_DifferentIsNew_ReturnsFalse()
|
||||
{
|
||||
var tValue = new TValue(12345, 100.0);
|
||||
var args1 = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
var args2 = new TValueEventArgs { Value = tValue, IsNew = false };
|
||||
|
||||
Assert.False(args1.Equals(args2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_Object_SameTValueEventArgs_ReturnsTrue()
|
||||
{
|
||||
var tValue = new TValue(12345, 100.0);
|
||||
var args1 = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
object args2 = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
|
||||
Assert.True(args1.Equals(args2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_Object_DifferentType_ReturnsFalse()
|
||||
{
|
||||
var args = new TValueEventArgs { Value = new TValue(12345, 100.0), IsNew = true };
|
||||
object other = "not a TValueEventArgs";
|
||||
|
||||
Assert.False(args.Equals(other));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_Object_Null_ReturnsFalse()
|
||||
{
|
||||
var args = new TValueEventArgs { Value = new TValue(12345, 100.0), IsNew = true };
|
||||
|
||||
Assert.False(args.Equals(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_SameValues_ReturnsSameHashCode()
|
||||
{
|
||||
var tValue = new TValue(12345, 100.0);
|
||||
var args1 = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
var args2 = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
|
||||
Assert.Equal(args1.GetHashCode(), args2.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_DifferentValues_ReturnsDifferentHashCode()
|
||||
{
|
||||
var tValue1 = new TValue(12345, 100.0);
|
||||
var tValue2 = new TValue(12346, 100.0);
|
||||
var args1 = new TValueEventArgs { Value = tValue1, IsNew = true };
|
||||
var args2 = new TValueEventArgs { Value = tValue2, IsNew = true };
|
||||
|
||||
Assert.NotEqual(args1.GetHashCode(), args2.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_DifferentIsNew_ReturnsDifferentHashCode()
|
||||
{
|
||||
var tValue = new TValue(12345, 100.0);
|
||||
var args1 = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
var args2 = new TValueEventArgs { Value = tValue, IsNew = false };
|
||||
|
||||
Assert.NotEqual(args1.GetHashCode(), args2.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualityOperator_SameValues_ReturnsTrue()
|
||||
{
|
||||
var tValue = new TValue(12345, 100.0);
|
||||
var args1 = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
var args2 = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
|
||||
Assert.True(args1 == args2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualityOperator_DifferentValues_ReturnsFalse()
|
||||
{
|
||||
var tValue1 = new TValue(12345, 100.0);
|
||||
var tValue2 = new TValue(12346, 100.0);
|
||||
var args1 = new TValueEventArgs { Value = tValue1, IsNew = true };
|
||||
var args2 = new TValueEventArgs { Value = tValue2, IsNew = true };
|
||||
|
||||
Assert.False(args1 == args2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InequalityOperator_SameValues_ReturnsFalse()
|
||||
{
|
||||
var tValue = new TValue(12345, 100.0);
|
||||
var args1 = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
var args2 = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
|
||||
Assert.False(args1 != args2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InequalityOperator_DifferentValues_ReturnsTrue()
|
||||
{
|
||||
var tValue1 = new TValue(12345, 100.0);
|
||||
var tValue2 = new TValue(12346, 100.0);
|
||||
var args1 = new TValueEventArgs { Value = tValue1, IsNew = true };
|
||||
var args2 = new TValueEventArgs { Value = tValue2, IsNew = true };
|
||||
|
||||
Assert.True(args1 != args2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_WithNaN_BothNaN_ReturnsTrue()
|
||||
{
|
||||
var tValue1 = new TValue(12345, double.NaN);
|
||||
var tValue2 = new TValue(12345, double.NaN);
|
||||
var args1 = new TValueEventArgs { Value = tValue1, IsNew = true };
|
||||
var args2 = new TValueEventArgs { Value = tValue2, IsNew = true };
|
||||
|
||||
Assert.True(args1.Equals(args2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_WithNaN_DoesNotThrow()
|
||||
{
|
||||
var tValue = new TValue(12345, double.NaN);
|
||||
var args = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
|
||||
var hash = args.GetHashCode();
|
||||
|
||||
Assert.True(hash != 0 || hash == 0); // Just verify it doesn't throw
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithZeroTime_Allowed()
|
||||
{
|
||||
var tValue = new TValue(0, 100.0);
|
||||
var args = new TValueEventArgs { Value = tValue, IsNew = false };
|
||||
|
||||
Assert.Equal(0, args.Value.Time);
|
||||
Assert.Equal(100.0, args.Value.Value);
|
||||
Assert.False(args.IsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNegativeTime_Allowed()
|
||||
{
|
||||
var tValue = new TValue(-12345, 100.0);
|
||||
var args = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
|
||||
Assert.Equal(-12345, args.Value.Time);
|
||||
Assert.Equal(100.0, args.Value.Value);
|
||||
Assert.True(args.IsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithMaxLongTime_Allowed()
|
||||
{
|
||||
var tValue = new TValue(long.MaxValue, 100.0);
|
||||
var args = new TValueEventArgs { Value = tValue, IsNew = false };
|
||||
|
||||
Assert.Equal(long.MaxValue, args.Value.Time);
|
||||
Assert.Equal(100.0, args.Value.Value);
|
||||
Assert.False(args.IsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithMaxDoubleValue_Allowed()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, double.MaxValue);
|
||||
var args = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
|
||||
Assert.Equal(double.MaxValue, args.Value.Value);
|
||||
Assert.True(args.IsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithMinDoubleValue_Allowed()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, double.MinValue);
|
||||
var args = new TValueEventArgs { Value = tValue, IsNew = false };
|
||||
|
||||
Assert.Equal(double.MinValue, args.Value.Value);
|
||||
Assert.False(args.IsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithEpsilonValue_Allowed()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, double.Epsilon);
|
||||
var args = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
|
||||
Assert.Equal(double.Epsilon, args.Value.Value);
|
||||
Assert.True(args.IsNew);
|
||||
}
|
||||
}
|
||||
|
||||
public class TValuePublishedHandlerTests
|
||||
{
|
||||
private class MockPublisher : ITValuePublisher
|
||||
{
|
||||
#pragma warning disable CS0067 // Event is never used - intentional for delegate testing
|
||||
public event TValuePublishedHandler? Pub;
|
||||
#pragma warning restore CS0067
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Delegate_CanBeAssigned()
|
||||
{
|
||||
TValuePublishedHandler handler = (_, in _) => { };
|
||||
|
||||
Assert.NotNull(handler);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Delegate_CanBeInvoked()
|
||||
{
|
||||
bool wasCalled = false;
|
||||
TValuePublishedHandler handler = (object? sender, in TValueEventArgs args) => wasCalled = true;
|
||||
|
||||
var args = new TValueEventArgs { Value = new TValue(12345, 100.0), IsNew = true };
|
||||
handler(null, args);
|
||||
|
||||
Assert.True(wasCalled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Delegate_ReceivesCorrectArguments()
|
||||
{
|
||||
object? receivedSender = null;
|
||||
TValueEventArgs receivedArgs = default;
|
||||
|
||||
TValuePublishedHandler handler = (object? sender, in TValueEventArgs args) =>
|
||||
{
|
||||
receivedSender = sender;
|
||||
receivedArgs = args;
|
||||
};
|
||||
|
||||
var publisher = new MockPublisher();
|
||||
var expectedArgs = new TValueEventArgs { Value = new TValue(12345, 100.0), IsNew = true };
|
||||
|
||||
handler(publisher, expectedArgs);
|
||||
|
||||
Assert.Equal(publisher, receivedSender);
|
||||
Assert.Equal(expectedArgs, receivedArgs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Delegate_CanBeNull()
|
||||
{
|
||||
TValuePublishedHandler? handler = null;
|
||||
|
||||
var exception = Record.Exception(() => handler?.Invoke(null, new TValueEventArgs()));
|
||||
Assert.Null(exception);
|
||||
}
|
||||
}
|
||||
|
||||
public class ITValuePublisherTests
|
||||
{
|
||||
private class MockPublisher : ITValuePublisher
|
||||
{
|
||||
public event TValuePublishedHandler? Pub;
|
||||
|
||||
public void RaiseEvent(TValue value, bool isNew = true)
|
||||
{
|
||||
Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew });
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Interface_CanBeImplemented()
|
||||
{
|
||||
ITValuePublisher publisher = new MockPublisher();
|
||||
|
||||
Assert.NotNull(publisher);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_CanBeSubscribed()
|
||||
{
|
||||
var publisher = new MockPublisher();
|
||||
bool eventRaised = false;
|
||||
|
||||
publisher.Pub += (object? sender, in TValueEventArgs args) => eventRaised = true;
|
||||
|
||||
publisher.RaiseEvent(new TValue(12345, 100.0));
|
||||
|
||||
Assert.True(eventRaised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_CanBeUnsubscribed()
|
||||
{
|
||||
var publisher = new MockPublisher();
|
||||
bool eventRaised = false;
|
||||
|
||||
TValuePublishedHandler handler = (object? sender, in TValueEventArgs args) => eventRaised = true;
|
||||
publisher.Pub += handler;
|
||||
|
||||
publisher.RaiseEvent(new TValue(12345, 100.0));
|
||||
Assert.True(eventRaised);
|
||||
|
||||
eventRaised = false;
|
||||
publisher.Pub -= handler;
|
||||
|
||||
publisher.RaiseEvent(new TValue(12346, 101.0));
|
||||
Assert.False(eventRaised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_MultipleSubscribers_AllReceiveEvent()
|
||||
{
|
||||
var publisher = new MockPublisher();
|
||||
bool event1Raised = false;
|
||||
bool event2Raised = false;
|
||||
|
||||
publisher.Pub += (object? sender, in TValueEventArgs args) => event1Raised = true;
|
||||
publisher.Pub += (object? sender, in TValueEventArgs args) => event2Raised = true;
|
||||
|
||||
publisher.RaiseEvent(new TValue(12345, 100.0));
|
||||
|
||||
Assert.True(event1Raised);
|
||||
Assert.True(event2Raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_NoSubscribers_DoesNotThrow()
|
||||
{
|
||||
var publisher = new MockPublisher();
|
||||
|
||||
var exception = Record.Exception(() => publisher.RaiseEvent(new TValue(12345, 100.0)));
|
||||
Assert.Null(exception);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_ReceivesCorrectSender()
|
||||
{
|
||||
var publisher = new MockPublisher();
|
||||
object? receivedSender = null;
|
||||
|
||||
publisher.Pub += (object? sender, in TValueEventArgs args) => receivedSender = sender;
|
||||
|
||||
publisher.RaiseEvent(new TValue(12345, 100.0));
|
||||
|
||||
Assert.Equal(publisher, receivedSender);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_ReceivesCorrectEventArgs()
|
||||
{
|
||||
var publisher = new MockPublisher();
|
||||
TValueEventArgs receivedArgs = default;
|
||||
|
||||
publisher.Pub += (object? sender, in TValueEventArgs args) => receivedArgs = args;
|
||||
|
||||
var expectedValue = new TValue(12345, 100.0);
|
||||
publisher.RaiseEvent(expectedValue, isNew: true);
|
||||
|
||||
Assert.Equal(expectedValue, receivedArgs.Value);
|
||||
Assert.True(receivedArgs.IsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_IsNew_False_ReceivedCorrectly()
|
||||
{
|
||||
var publisher = new MockPublisher();
|
||||
TValueEventArgs receivedArgs = default;
|
||||
|
||||
publisher.Pub += (object? sender, in TValueEventArgs args) => receivedArgs = args;
|
||||
|
||||
var expectedValue = new TValue(12345, 100.0);
|
||||
publisher.RaiseEvent(expectedValue, isNew: false);
|
||||
|
||||
Assert.Equal(expectedValue, receivedArgs.Value);
|
||||
Assert.False(receivedArgs.IsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_HandlerThrows_ExceptionPropagates()
|
||||
{
|
||||
var publisher = new MockPublisher();
|
||||
|
||||
publisher.Pub += (object? sender, in TValueEventArgs args) =>
|
||||
{
|
||||
throw new InvalidOperationException("Test exception");
|
||||
};
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
publisher.RaiseEvent(new TValue(12345, 100.0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_HandlerWithNaNValue_ReceivesNaN()
|
||||
{
|
||||
var publisher = new MockPublisher();
|
||||
double receivedValue = 0;
|
||||
|
||||
publisher.Pub += (object? sender, in TValueEventArgs args) => receivedValue = args.Value.Value;
|
||||
|
||||
publisher.RaiseEvent(new TValue(12345, double.NaN));
|
||||
|
||||
Assert.True(double.IsNaN(receivedValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_HandlerWithInfinityValue_ReceivesInfinity()
|
||||
{
|
||||
var publisher = new MockPublisher();
|
||||
double receivedValue = 0;
|
||||
|
||||
publisher.Pub += (object? sender, in TValueEventArgs args) => receivedValue = args.Value.Value;
|
||||
|
||||
publisher.RaiseEvent(new TValue(12345, double.PositiveInfinity));
|
||||
|
||||
Assert.True(double.IsPositiveInfinity(receivedValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_MultipleEvents_AllReceived()
|
||||
{
|
||||
var publisher = new MockPublisher();
|
||||
var receivedValues = new List<double>();
|
||||
|
||||
publisher.Pub += (object? sender, in TValueEventArgs args) => receivedValues.Add(args.Value.Value);
|
||||
|
||||
publisher.RaiseEvent(new TValue(12345, 100.0));
|
||||
publisher.RaiseEvent(new TValue(12346, 200.0));
|
||||
publisher.RaiseEvent(new TValue(12347, 300.0));
|
||||
|
||||
Assert.Equal(3, receivedValues.Count);
|
||||
Assert.Equal(100.0, receivedValues[0]);
|
||||
Assert.Equal(200.0, receivedValues[1]);
|
||||
Assert.Equal(300.0, receivedValues[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_SubscribeUnsubscribeMultipleTimes_Works()
|
||||
{
|
||||
var publisher = new MockPublisher();
|
||||
int callCount = 0;
|
||||
|
||||
TValuePublishedHandler handler = (object? sender, in TValueEventArgs args) => callCount++;
|
||||
|
||||
// Subscribe multiple times
|
||||
publisher.Pub += handler;
|
||||
publisher.Pub += handler;
|
||||
|
||||
publisher.RaiseEvent(new TValue(12345, 100.0));
|
||||
Assert.Equal(2, callCount); // Called twice
|
||||
|
||||
callCount = 0;
|
||||
// Unsubscribe once
|
||||
publisher.Pub -= handler;
|
||||
|
||||
publisher.RaiseEvent(new TValue(12346, 200.0));
|
||||
Assert.Equal(1, callCount); // Called once
|
||||
|
||||
callCount = 0;
|
||||
// Unsubscribe remaining
|
||||
publisher.Pub -= handler;
|
||||
|
||||
publisher.RaiseEvent(new TValue(12347, 300.0));
|
||||
Assert.Equal(0, callCount); // Not called
|
||||
}
|
||||
}
|
||||
@@ -31,12 +31,12 @@ public class AdxTests
|
||||
adx.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Update with 100th point (isNew=true)
|
||||
adx.Update(bars[99], true);
|
||||
// Update with 100th point (isNew=true is default, so omit it)
|
||||
adx.Update(bars[99]);
|
||||
|
||||
// Update with modified 100th point (isNew=false)
|
||||
var modifiedBar = new TBar(bars[99].Time, bars[99].Open, bars[99].High + 1.0, bars[99].Low - 1.0, bars[99].Close, bars[99].Volume);
|
||||
var val2 = adx.Update(modifiedBar, false);
|
||||
var val2 = adx.Update(modifiedBar, isNew: false);
|
||||
|
||||
// Create new instance and feed up to modified
|
||||
var adx2 = new Adx(14);
|
||||
@@ -44,7 +44,7 @@ public class AdxTests
|
||||
{
|
||||
adx2.Update(bars[i]);
|
||||
}
|
||||
var val3 = adx2.Update(modifiedBar, true);
|
||||
var val3 = adx2.Update(modifiedBar);
|
||||
|
||||
Assert.Equal(val3.Value, val2.Value, 1e-9);
|
||||
Assert.Equal(adx2.DiPlus.Value, adx.DiPlus.Value, 1e-9);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Skender.Stock.Indicators;
|
||||
using TALib;
|
||||
using Tulip;
|
||||
using OoplesFinance.StockIndicators;
|
||||
using OoplesFinance.StockIndicators.Models;
|
||||
using OoplesFinance.StockIndicators.Enums;
|
||||
|
||||
@@ -10,9 +10,9 @@ public class AdxrTests
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
adxr.Update(bars[i]);
|
||||
adxr.Update(bar);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(adxr.Last.Value));
|
||||
@@ -56,9 +56,9 @@ public class AdxrTests
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
adxr.Update(bars[i]);
|
||||
adxr.Update(bar);
|
||||
}
|
||||
|
||||
adxr.Reset();
|
||||
@@ -66,9 +66,9 @@ public class AdxrTests
|
||||
Assert.False(adxr.IsHot);
|
||||
|
||||
// Feed again
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
adxr.Update(bars[i]);
|
||||
adxr.Update(bar);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(adxr.Last.Value));
|
||||
@@ -82,9 +82,9 @@ public class AdxrTests
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var streamingResults = new List<double>();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
streamingResults.Add(adxr.Update(bars[i]).Value);
|
||||
streamingResults.Add(adxr.Update(bar).Value);
|
||||
}
|
||||
|
||||
var adxr2 = new Adxr(14);
|
||||
@@ -105,9 +105,9 @@ public class AdxrTests
|
||||
|
||||
var adxr = new Adxr(14);
|
||||
var streamingResults = new List<double>();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
streamingResults.Add(adxr.Update(bars[i]).Value);
|
||||
streamingResults.Add(adxr.Update(bar).Value);
|
||||
}
|
||||
|
||||
var staticResults = Adxr.Batch(bars, 14);
|
||||
@@ -253,9 +253,9 @@ public class AdxrTests
|
||||
|
||||
// 2. Streaming Mode (instance, one bar at a time)
|
||||
var streamingInd = new Adxr(period);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
streamingInd.Update(bars[i]);
|
||||
streamingInd.Update(bar);
|
||||
}
|
||||
double streamingResult = streamingInd.Last.Value;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
@@ -107,7 +106,7 @@ public sealed class Adxr : ITValuePublisher
|
||||
|
||||
_adxHistory.Add(currentAdx);
|
||||
|
||||
double adxr = 0;
|
||||
double adxr;
|
||||
// We calculate ADXR even if not fully hot, as long as we have history
|
||||
if (!double.IsNaN(prevAdx))
|
||||
{
|
||||
|
||||
@@ -2,26 +2,19 @@ using OoplesFinance.StockIndicators;
|
||||
using OoplesFinance.StockIndicators.Models;
|
||||
using Skender.Stock.Indicators;
|
||||
using TALib;
|
||||
using Tulip;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class RsiValidationTests : IDisposable
|
||||
public sealed class RsiValidationTests(ITestOutputHelper output) : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _testData;
|
||||
private readonly ITestOutputHelper _output;
|
||||
private readonly ValidationTestData _testData = new();
|
||||
private readonly ITestOutputHelper _output = output;
|
||||
private bool _disposed;
|
||||
|
||||
public RsiValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
_testData = new ValidationTestData();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
Dispose(disposing: true);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
@@ -60,27 +53,25 @@ public sealed class RsiValidationTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_Streaming()
|
||||
public void Validate_Talib_Span()
|
||||
{
|
||||
int[] periods = { 9, 14, 25 };
|
||||
int[] periods = { 14, 20, 50, 100 };
|
||||
double[] tData = _testData.RawData.ToArray();
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib RSI (streaming)
|
||||
var rsi = new global::QuanTAlib.Rsi(period);
|
||||
var qResults = new List<double>();
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
qResults.Add(rsi.Update(item).Value);
|
||||
}
|
||||
double[] qOutput = new double[tData.Length];
|
||||
Rsi.Calculate(tData.AsSpan(), qOutput.AsSpan(), period);
|
||||
|
||||
// Calculate Skender RSI
|
||||
var sResult = _testData.SkenderQuotes.GetRsi(period).ToList();
|
||||
double[] tOutput = new double[tData.Length];
|
||||
var retCode = TALib.Functions.Rsi<double>(tData, 0..^0, tOutput, out var outRange, period);
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
|
||||
// Compare last 100 records
|
||||
ValidationHelper.VerifyData(qResults, sResult, (s) => s.Rsi);
|
||||
int lookback = TALib.Functions.RsiLookback(period);
|
||||
|
||||
QuanTAlib.Tests.ValidationHelper.VerifyData(qOutput, tOutput, outRange, lookback);
|
||||
}
|
||||
_output.WriteLine("RSI Streaming validated successfully against Skender");
|
||||
_output.WriteLine("RSI Span validated against TA-Lib");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -107,30 +98,28 @@ public sealed class RsiValidationTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Talib_Batch()
|
||||
public void Validate_Tulip_Span()
|
||||
{
|
||||
int[] periods = { 9, 14, 25 };
|
||||
|
||||
// Prepare data for TA-Lib (double[])
|
||||
int[] periods = { 14, 20, 50, 100 };
|
||||
double[] tData = _testData.RawData.ToArray();
|
||||
double[] output = new double[tData.Length];
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib RSI (batch TSeries)
|
||||
var rsi = new global::QuanTAlib.Rsi(period);
|
||||
var qResult = rsi.Update(_testData.Data);
|
||||
double[] qOutput = new double[tData.Length];
|
||||
Rsi.Calculate(tData.AsSpan(), qOutput.AsSpan(), period);
|
||||
|
||||
// Calculate TA-Lib RSI
|
||||
var retCode = TALib.Functions.Rsi<double>(tData, 0..^0, output, out var outRange, period);
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
var rsiIndicator = Tulip.Indicators.rsi;
|
||||
double[][] inputs = { tData };
|
||||
double[] options = { period };
|
||||
int lookback = period;
|
||||
double[][] outputs = { new double[tData.Length - lookback] };
|
||||
|
||||
int lookback = TALib.Functions.RsiLookback(period);
|
||||
rsiIndicator.Run(inputs, options, outputs);
|
||||
var tResult = outputs[0];
|
||||
|
||||
// Compare last 100 records
|
||||
ValidationHelper.VerifyData(qResult, output, outRange, lookback);
|
||||
QuanTAlib.Tests.ValidationHelper.VerifyData(qOutput, tResult, lookback);
|
||||
}
|
||||
_output.WriteLine("RSI Batch(TSeries) validated successfully against TA-Lib");
|
||||
_output.WriteLine("RSI Span validated against Tulip");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -140,7 +129,7 @@ public sealed class RsiValidationTests : IDisposable
|
||||
|
||||
// Prepare data for TA-Lib (double[])
|
||||
double[] tData = _testData.RawData.ToArray();
|
||||
double[] output = new double[tData.Length];
|
||||
double[] tOutput = new double[tData.Length];
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
@@ -153,13 +142,13 @@ public sealed class RsiValidationTests : IDisposable
|
||||
}
|
||||
|
||||
// Calculate TA-Lib RSI
|
||||
var retCode = TALib.Functions.Rsi<double>(tData, 0..^0, output, out var outRange, period);
|
||||
var retCode = TALib.Functions.Rsi<double>(tData, 0..^0, tOutput, out var outRange, period);
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = TALib.Functions.RsiLookback(period);
|
||||
|
||||
// Compare last 100 records
|
||||
ValidationHelper.VerifyData(qResults, output, outRange, lookback);
|
||||
ValidationHelper.VerifyData(qResults, tOutput, outRange, lookback);
|
||||
}
|
||||
_output.WriteLine("RSI Streaming validated successfully against TA-Lib");
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@ namespace QuanTAlib.Tests;
|
||||
|
||||
public class VelTests
|
||||
{
|
||||
// Expected values for VEL(3) with input [10, 20, 30]
|
||||
// PWMA(3) = 360/14, WMA(3) = 140/6, VEL = PWMA - WMA
|
||||
private const double ExpectedPwma = 360.0 / 14.0; // 25.7142857...
|
||||
private const double ExpectedWma = 140.0 / 6.0; // 23.3333333...
|
||||
private const double ExpectedVel = ExpectedPwma - ExpectedWma; // 2.38095238...
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidPeriod_ThrowsArgumentException()
|
||||
{
|
||||
@@ -31,14 +37,14 @@ public class VelTests
|
||||
var vel = new Vel(10);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Update with isNew=true
|
||||
var val1 = vel.Update(new TValue(time, 100), true);
|
||||
// Update with default isNew=true
|
||||
var val1 = vel.Update(new TValue(time, 100));
|
||||
|
||||
// Update with isNew=false (same time, different value)
|
||||
vel.Update(new TValue(time, 105), false);
|
||||
vel.Update(new TValue(time, 105), isNew: false);
|
||||
|
||||
// Update with isNew=false (same time, original value) - should match val1 if state rollback works
|
||||
var val3 = vel.Update(new TValue(time, 100), false);
|
||||
var val3 = vel.Update(new TValue(time, 100), isNew: false);
|
||||
|
||||
Assert.Equal(val1.Value, val3.Value, 1e-9);
|
||||
}
|
||||
@@ -96,11 +102,7 @@ public class VelTests
|
||||
// WMA(3) of 10,20,30 = 140/6 = 23.3333333...
|
||||
// VEL = PWMA - WMA = 2.38095238...
|
||||
|
||||
double expectedPwma = 360.0 / 14.0;
|
||||
double expectedWma = 140.0 / 6.0;
|
||||
double expectedVel = expectedPwma - expectedWma;
|
||||
|
||||
Assert.Equal(expectedVel, vel.Last.Value, 1e-10);
|
||||
Assert.Equal(ExpectedVel, vel.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -115,11 +117,7 @@ public class VelTests
|
||||
|
||||
Assert.Equal(3, results.Count);
|
||||
|
||||
double expectedPwma = 360.0 / 14.0;
|
||||
double expectedWma = 140.0 / 6.0;
|
||||
double expectedVel = expectedPwma - expectedWma;
|
||||
|
||||
Assert.Equal(expectedVel, results.Last.Value, 1e-10);
|
||||
Assert.Equal(ExpectedVel, results.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,31 +1,366 @@
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class VelValidationTests
|
||||
public sealed class VelValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _testData;
|
||||
private bool _disposed;
|
||||
|
||||
public VelValidationTests()
|
||||
{
|
||||
_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 Vel_Matches_PwmaMinusWma()
|
||||
public void Vel_Matches_PwmaMinusWma_Batch()
|
||||
{
|
||||
// VEL = PWMA - WMA
|
||||
// We validate this relationship holds true for a random sequence of data.
|
||||
// Validate this relationship holds for batch calculation
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
int period = 10;
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var vel = new Vel(period);
|
||||
var pwma = new Pwma(period);
|
||||
var wma = new Wma(period);
|
||||
|
||||
var velResult = vel.Update(_testData.Data);
|
||||
var pwmaResult = pwma.Update(_testData.Data);
|
||||
var wmaResult = wma.Update(_testData.Data);
|
||||
|
||||
Assert.Equal(_testData.Data.Count, velResult.Count);
|
||||
Assert.Equal(_testData.Data.Count, pwmaResult.Count);
|
||||
Assert.Equal(_testData.Data.Count, wmaResult.Count);
|
||||
|
||||
// Verify relationship for all data points
|
||||
for (int i = 0; i < _testData.Data.Count; i++)
|
||||
{
|
||||
double expected = pwmaResult[i].Value - wmaResult[i].Value;
|
||||
Assert.Equal(expected, velResult[i].Value, 1e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vel_Matches_PwmaMinusWma_Streaming()
|
||||
{
|
||||
// VEL = PWMA - WMA
|
||||
// Validate this relationship holds for streaming calculation
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var vel = new Vel(period);
|
||||
var pwma = new Pwma(period);
|
||||
var wma = new Wma(period);
|
||||
|
||||
for (int i = 0; i < _testData.Data.Count; i++)
|
||||
{
|
||||
var input = _testData.Data[i];
|
||||
var v = vel.Update(input);
|
||||
var p = pwma.Update(input);
|
||||
var w = wma.Update(input);
|
||||
|
||||
double expected = p.Value - w.Value;
|
||||
Assert.Equal(expected, v.Value, 1e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vel_Matches_PwmaMinusWma_Span()
|
||||
{
|
||||
// VEL = PWMA - WMA
|
||||
// Validate this relationship holds for span calculation
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
double[] sourceData = _testData.RawData.ToArray();
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
double[] velOutput = new double[sourceData.Length];
|
||||
double[] pwmaOutput = new double[sourceData.Length];
|
||||
double[] wmaOutput = new double[sourceData.Length];
|
||||
|
||||
Vel.Batch(sourceData.AsSpan(), velOutput.AsSpan(), period);
|
||||
Pwma.Calculate(sourceData.AsSpan(), pwmaOutput.AsSpan(), period);
|
||||
Wma.Batch(sourceData.AsSpan(), wmaOutput.AsSpan(), period);
|
||||
|
||||
// Verify relationship for all data points
|
||||
for (int i = 0; i < sourceData.Length; i++)
|
||||
{
|
||||
double expected = pwmaOutput[i] - wmaOutput[i];
|
||||
Assert.Equal(expected, velOutput[i], 1e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vel_AllModes_ProduceIdenticalResults()
|
||||
{
|
||||
// Critical validation: All 3 API modes must produce identical results
|
||||
int[] periods = { 5, 10, 20, 50 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// 1. Batch Mode (TSeries)
|
||||
var batchVel = new Vel(period);
|
||||
var batchResult = batchVel.Update(_testData.Data);
|
||||
|
||||
// 2. Span Mode
|
||||
double[] sourceData = _testData.RawData.ToArray();
|
||||
double[] spanOutput = new double[sourceData.Length];
|
||||
Vel.Batch(sourceData.AsSpan(), spanOutput.AsSpan(), period);
|
||||
|
||||
// 3. Streaming Mode
|
||||
var streamingVel = new Vel(period);
|
||||
var streamingResults = new List<double>();
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
streamingResults.Add(streamingVel.Update(item).Value);
|
||||
}
|
||||
|
||||
// Compare all modes (allow 1e-8 tolerance for accumulated floating-point errors)
|
||||
for (int i = 0; i < _testData.Data.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult[i].Value, spanOutput[i], 1e-8);
|
||||
Assert.Equal(batchResult[i].Value, streamingResults[i], 1e-8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vel_Convergence_AfterWarmup()
|
||||
{
|
||||
// After warmup period, indicator should be "hot" and producing stable values
|
||||
int[] periods = { 5, 10, 20, 50 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var vel = new Vel(period);
|
||||
|
||||
Assert.False(vel.IsHot);
|
||||
|
||||
// Feed period number of bars
|
||||
for (int i = 0; i < period - 1; i++)
|
||||
{
|
||||
vel.Update(_testData.Data[i]);
|
||||
Assert.False(vel.IsHot);
|
||||
}
|
||||
|
||||
vel.Update(_testData.Data[period - 1]);
|
||||
Assert.True(vel.IsHot);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vel_HandlesNaN_Gracefully()
|
||||
{
|
||||
var vel = new Vel(10);
|
||||
|
||||
// Feed some valid data
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
vel.Update(_testData.Data[i]);
|
||||
}
|
||||
|
||||
// Feed NaN
|
||||
var result = vel.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
|
||||
// Continue with valid data
|
||||
for (int i = 20; i < 30; i++)
|
||||
{
|
||||
var r = vel.Update(_testData.Data[i]);
|
||||
Assert.True(double.IsFinite(r.Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vel_HandlesInfinity_Gracefully()
|
||||
{
|
||||
var vel = new Vel(10);
|
||||
|
||||
// Feed some valid data
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
vel.Update(_testData.Data[i]);
|
||||
}
|
||||
|
||||
// Feed Infinity
|
||||
var resultPos = vel.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(resultPos.Value));
|
||||
|
||||
var resultNeg = vel.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
|
||||
Assert.True(double.IsFinite(resultNeg.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vel_ZeroCrossing_DetectsDirectionChange()
|
||||
{
|
||||
// VEL crossing zero indicates momentum direction change
|
||||
var vel = new Vel(5);
|
||||
|
||||
// Create uptrend data
|
||||
double[] uptrend = { 100, 102, 104, 106, 108, 110 };
|
||||
foreach (var price in uptrend)
|
||||
{
|
||||
vel.Update(new TValue(DateTime.UtcNow, price));
|
||||
}
|
||||
double uptrendVel = vel.Last.Value;
|
||||
Assert.True(uptrendVel > 0, "Uptrend should produce positive VEL");
|
||||
|
||||
// Create downtrend data
|
||||
double[] downtrend = { 110, 108, 106, 104, 102, 100 };
|
||||
foreach (var price in downtrend)
|
||||
{
|
||||
vel.Update(new TValue(DateTime.UtcNow, price));
|
||||
}
|
||||
double downtrendVel = vel.Last.Value;
|
||||
Assert.True(downtrendVel < 0, "Downtrend should produce negative VEL");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vel_FlatLine_ProducesZeroVelocity()
|
||||
{
|
||||
// Flat price should produce zero velocity
|
||||
var vel = new Vel(10);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
vel.Update(new TValue(DateTime.UtcNow, 100));
|
||||
}
|
||||
|
||||
// After sufficient warmup, flat line should produce VEL ≈ 0
|
||||
Assert.True(Math.Abs(vel.Last.Value) < 1e-10,
|
||||
$"Expected VEL ≈ 0 for flat line, got {vel.Last.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vel_LargeDataset_MaintainsPrecision()
|
||||
{
|
||||
// Test with large dataset to ensure no drift
|
||||
int period = 20;
|
||||
var vel = new Vel(period);
|
||||
var pwma = new Pwma(period);
|
||||
var wma = new Wma(period);
|
||||
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
var bars = gbm.Fetch(10000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
for (int i = 0; i < bars.Close.Count; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
var input = new TValue(bar.Time, bar.Close);
|
||||
|
||||
var input = bars.Close[i];
|
||||
var v = vel.Update(input);
|
||||
var p = pwma.Update(input);
|
||||
var w = wma.Update(input);
|
||||
|
||||
Assert.Equal(p.Value - w.Value, v.Value, ValidationHelper.DefaultTolerance);
|
||||
// Every 1000th point, verify precision
|
||||
if (i % 1000 == 0 && i > period)
|
||||
{
|
||||
double expected = p.Value - w.Value;
|
||||
Assert.Equal(expected, v.Value, 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vel_DifferentPeriods_ProduceDifferentSensitivity()
|
||||
{
|
||||
// Shorter periods should be more sensitive to price changes
|
||||
var vel5 = new Vel(5);
|
||||
var vel20 = new Vel(20);
|
||||
var vel50 = new Vel(50);
|
||||
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.3, seed: 123);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars.Close)
|
||||
{
|
||||
vel5.Update(bar);
|
||||
vel20.Update(bar);
|
||||
vel50.Update(bar);
|
||||
}
|
||||
|
||||
// Calculate average absolute velocity (measure of sensitivity)
|
||||
double avgVel5 = 0, avgVel20 = 0, avgVel50 = 0;
|
||||
int count = 0;
|
||||
|
||||
vel5 = new Vel(5);
|
||||
vel20 = new Vel(20);
|
||||
vel50 = new Vel(50);
|
||||
|
||||
foreach (var bar in bars.Close)
|
||||
{
|
||||
vel5.Update(bar);
|
||||
vel20.Update(bar);
|
||||
vel50.Update(bar);
|
||||
|
||||
if (vel5.IsHot && vel20.IsHot && vel50.IsHot)
|
||||
{
|
||||
avgVel5 += Math.Abs(vel5.Last.Value);
|
||||
avgVel20 += Math.Abs(vel20.Last.Value);
|
||||
avgVel50 += Math.Abs(vel50.Last.Value);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
avgVel5 /= count;
|
||||
avgVel20 /= count;
|
||||
avgVel50 /= count;
|
||||
|
||||
// All periods should produce finite numeric results
|
||||
Assert.True(double.IsFinite(avgVel5));
|
||||
Assert.True(double.IsFinite(avgVel20));
|
||||
Assert.True(double.IsFinite(avgVel50));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vel_BatchSpan_HandlesNaN_InMiddle()
|
||||
{
|
||||
double[] data = new double[100];
|
||||
var gbm = new GBM(startPrice: 100, seed: 42);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
data[i] = gbm.Next().Close;
|
||||
}
|
||||
|
||||
// Insert NaN in the middle
|
||||
data[50] = double.NaN;
|
||||
|
||||
double[] output = new double[100];
|
||||
Vel.Batch(data.AsSpan(), output.AsSpan(), 10);
|
||||
|
||||
// All outputs should be finite
|
||||
foreach (var value in output)
|
||||
{
|
||||
Assert.True(double.IsFinite(value), $"Expected finite value, got {value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vel_EdgeCase_Period1()
|
||||
{
|
||||
// Period=1 should still work (though not very useful)
|
||||
var vel = new Vel(1);
|
||||
|
||||
vel.Update(new TValue(DateTime.UtcNow, 100));
|
||||
// PWMA(1) = 100, WMA(1) = 100, VEL = 0
|
||||
Assert.Equal(0, vel.Last.Value, 1e-10);
|
||||
|
||||
vel.Update(new TValue(DateTime.UtcNow, 110));
|
||||
// PWMA(1) = 110, WMA(1) = 110, VEL = 0
|
||||
Assert.Equal(0, vel.Last.Value, 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@ Statistical analysis tools applied to price/returns.
|
||||
| [SKEW](skew/Skew.md) | Skewness | |
|
||||
| SPEARMAN | Spearman Rank Correlation | |
|
||||
| [STDDEV](stddev/StdDev.md) | Standard Deviation | |
|
||||
| [SUM](sum/Sum.md) | Rolling Sum | Kahan-Babuška summation for numerical stability. |
|
||||
| THEIL | Theil Index | |
|
||||
| [VARIANCE](variance/Variance.md) | Variance | |
|
||||
| ZSCORE | Z-score standardization | |
|
||||
| ZTEST | Z-Test | |
|
||||
| ZTEST | Z-Test | |
|
||||
|
||||
@@ -176,7 +176,7 @@ public class StdDevTests
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueAfterPeriod()
|
||||
{
|
||||
int period = 5;
|
||||
const int period = 5;
|
||||
var stdDev = new StdDev(period);
|
||||
|
||||
for (int i = 0; i < period; i++)
|
||||
|
||||
@@ -1,126 +1,517 @@
|
||||
using QuanTAlib.Tests;
|
||||
using Skender.Stock.Indicators;
|
||||
using TALib;
|
||||
using Tulip;
|
||||
using OoplesFinance.StockIndicators;
|
||||
using OoplesFinance.StockIndicators.Models;
|
||||
using OoplesFinance.StockIndicators.Enums;
|
||||
using MathNet.Numerics.Statistics;
|
||||
|
||||
namespace QuanTAlib.Validation;
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class StdDevValidationTests
|
||||
public sealed class StdDevValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _data = new();
|
||||
private readonly ValidationTestData _testData;
|
||||
private bool _disposed;
|
||||
|
||||
public StdDevValidationTests()
|
||||
{
|
||||
_testData = new ValidationTestData();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
if (disposing) _testData?.Dispose();
|
||||
}
|
||||
|
||||
#region Skender Validation
|
||||
|
||||
[Fact]
|
||||
public void StdDev_Matches_Skender()
|
||||
public void StdDev_Matches_Skender_Batch()
|
||||
{
|
||||
// Skender StdDev uses Population Standard Deviation (N)
|
||||
int period = 20;
|
||||
var stdDev = new StdDev(period, isPopulation: true);
|
||||
var skenderStdDev = _data.SkenderQuotes.GetStdDev(period);
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
var skenderList = skenderStdDev.ToList();
|
||||
var quotes = _data.SkenderQuotes.ToList();
|
||||
|
||||
for (int i = 0; i < quotes.Count; i++)
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var tValue = stdDev.Update(new TValue(quotes[i].Date, (double)quotes[i].Close));
|
||||
var skenderVal = skenderList[i].StdDev;
|
||||
var stdDev = new StdDev(period, isPopulation: true);
|
||||
var qResult = stdDev.Update(_testData.Data);
|
||||
|
||||
if (i >= period && skenderVal.HasValue)
|
||||
{
|
||||
Assert.Equal(skenderVal.Value, tValue.Value, ValidationHelper.DefaultTolerance);
|
||||
}
|
||||
var sResult = _testData.SkenderQuotes.GetStdDev(period).ToList();
|
||||
|
||||
ValidationHelper.VerifyData(qResult, sResult, (s) => s.StdDev);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDev_Matches_Talib()
|
||||
public void StdDev_Matches_Skender_Streaming()
|
||||
{
|
||||
// Skender StdDev uses Population Standard Deviation (N)
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var stdDev = new StdDev(period, isPopulation: true);
|
||||
var qResults = new List<double>();
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
qResults.Add(stdDev.Update(item).Value);
|
||||
}
|
||||
|
||||
var sResult = _testData.SkenderQuotes.GetStdDev(period).ToList();
|
||||
|
||||
ValidationHelper.VerifyData(qResults, sResult, (s) => s.StdDev);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDev_Matches_Skender_Span()
|
||||
{
|
||||
// Skender StdDev uses Population Standard Deviation (N)
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
double[] sourceData = _testData.RawData.ToArray();
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
double[] qOutput = new double[sourceData.Length];
|
||||
StdDev.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period, isPopulation: true);
|
||||
|
||||
var sResult = _testData.SkenderQuotes.GetStdDev(period).ToList();
|
||||
|
||||
ValidationHelper.VerifyData(qOutput, sResult, (s) => s.StdDev);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TA-Lib Validation
|
||||
|
||||
[Fact]
|
||||
public void StdDev_Matches_Talib_Batch()
|
||||
{
|
||||
// TA-Lib STDDEV uses Population Standard Deviation (N)
|
||||
int period = 20;
|
||||
var stdDev = new StdDev(period, isPopulation: true);
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
double[] tData = _testData.RawData.ToArray();
|
||||
double[] output = new double[tData.Length];
|
||||
|
||||
var quotes = _data.SkenderQuotes.ToList();
|
||||
double[] input = quotes.Select(q => (double)q.Close).ToArray();
|
||||
double[] output = new double[input.Length];
|
||||
|
||||
// TA-Lib calculation
|
||||
// STDDEV(real, timeperiod=5, nbdev=1)
|
||||
var retCode = TALib.Functions.StdDev(input, 0..^0, output, out var outRange, period, 1.0);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
for (int i = 0; i < quotes.Count; i++)
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var tValue = stdDev.Update(new TValue(quotes[i].Date, (double)quotes[i].Close));
|
||||
var stdDev = new StdDev(period, isPopulation: true);
|
||||
var qResult = stdDev.Update(_testData.Data);
|
||||
|
||||
if (i >= outRange.Start.Value)
|
||||
{
|
||||
double talibVal = output[i - outRange.Start.Value];
|
||||
Assert.Equal(talibVal, tValue.Value, ValidationHelper.DefaultTolerance);
|
||||
}
|
||||
var retCode = TALib.Functions.StdDev(tData, 0..^0, output, out var outRange, period, 1.0);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = TALib.Functions.StdDevLookback(period);
|
||||
|
||||
ValidationHelper.VerifyData(qResult, output, outRange, lookback);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDev_Matches_Tulip()
|
||||
public void StdDev_Matches_Talib_Streaming()
|
||||
{
|
||||
// TA-Lib STDDEV uses Population Standard Deviation (N)
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
double[] tData = _testData.RawData.ToArray();
|
||||
double[] output = new double[tData.Length];
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var stdDev = new StdDev(period, isPopulation: true);
|
||||
var qResults = new List<double>();
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
qResults.Add(stdDev.Update(item).Value);
|
||||
}
|
||||
|
||||
var retCode = TALib.Functions.StdDev(tData, 0..^0, output, out var outRange, period, 1.0);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = TALib.Functions.StdDevLookback(period);
|
||||
|
||||
ValidationHelper.VerifyData(qResults, output, outRange, lookback);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDev_Matches_Talib_Span()
|
||||
{
|
||||
// TA-Lib STDDEV uses Population Standard Deviation (N)
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
double[] sourceData = _testData.RawData.ToArray();
|
||||
double[] output = new double[sourceData.Length];
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
double[] qOutput = new double[sourceData.Length];
|
||||
StdDev.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period, isPopulation: true);
|
||||
|
||||
var retCode = TALib.Functions.StdDev(sourceData, 0..^0, output, out var outRange, period, 1.0);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = TALib.Functions.StdDevLookback(period);
|
||||
|
||||
ValidationHelper.VerifyData(qOutput, output, outRange, lookback);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Tulip Validation
|
||||
|
||||
[Fact]
|
||||
public void StdDev_Matches_Tulip_Batch()
|
||||
{
|
||||
// Tulip STDDEV uses Population Standard Deviation (N)
|
||||
int period = 20;
|
||||
var stdDev = new StdDev(period, isPopulation: true);
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
double[] tData = _testData.RawData.ToArray();
|
||||
|
||||
var quotes = _data.SkenderQuotes.ToList();
|
||||
double[] input = quotes.Select(q => (double)q.Close).ToArray();
|
||||
|
||||
// Tulip calculation
|
||||
var stdDevInd = Tulip.Indicators.stddev;
|
||||
double[][] inputs = { input };
|
||||
double[] options = { period };
|
||||
double[][] outputs = { new double[input.Length - stdDevInd.Start(options)] };
|
||||
|
||||
stdDevInd.Run(inputs, options, outputs);
|
||||
|
||||
double[] output = outputs[0];
|
||||
int lookback = stdDevInd.Start(options);
|
||||
|
||||
for (int i = 0; i < quotes.Count; i++)
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var tValue = stdDev.Update(new TValue(quotes[i].Date, (double)quotes[i].Close));
|
||||
var stdDev = new StdDev(period, isPopulation: true);
|
||||
var qResult = stdDev.Update(_testData.Data);
|
||||
|
||||
if (i >= lookback)
|
||||
{
|
||||
double tulipVal = output[i - lookback];
|
||||
Assert.Equal(tulipVal, tValue.Value, ValidationHelper.DefaultTolerance);
|
||||
}
|
||||
var stdDevInd = Tulip.Indicators.stddev;
|
||||
double[][] inputs = { tData };
|
||||
double[] options = { period };
|
||||
int lookback = stdDevInd.Start(options);
|
||||
double[][] outputs = { new double[tData.Length - lookback] };
|
||||
|
||||
stdDevInd.Run(inputs, options, outputs);
|
||||
var tResult = outputs[0];
|
||||
|
||||
ValidationHelper.VerifyData(qResult, tResult, lookback);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDev_Matches_MathNet()
|
||||
public void StdDev_Matches_Tulip_Streaming()
|
||||
{
|
||||
// Tulip STDDEV uses Population Standard Deviation (N)
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
double[] tData = _testData.RawData.ToArray();
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var stdDev = new StdDev(period, isPopulation: true);
|
||||
var qResults = new List<double>();
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
qResults.Add(stdDev.Update(item).Value);
|
||||
}
|
||||
|
||||
var stdDevInd = Tulip.Indicators.stddev;
|
||||
double[][] inputs = { tData };
|
||||
double[] options = { period };
|
||||
int lookback = stdDevInd.Start(options);
|
||||
double[][] outputs = { new double[tData.Length - lookback] };
|
||||
|
||||
stdDevInd.Run(inputs, options, outputs);
|
||||
var tResult = outputs[0];
|
||||
|
||||
ValidationHelper.VerifyData(qResults, tResult, lookback);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDev_Matches_Tulip_Span()
|
||||
{
|
||||
// Tulip STDDEV uses Population Standard Deviation (N)
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
double[] sourceData = _testData.RawData.ToArray();
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
double[] qOutput = new double[sourceData.Length];
|
||||
StdDev.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period, isPopulation: true);
|
||||
|
||||
var stdDevInd = Tulip.Indicators.stddev;
|
||||
double[][] inputs = { sourceData };
|
||||
double[] options = { period };
|
||||
int lookback = stdDevInd.Start(options);
|
||||
double[][] outputs = { new double[sourceData.Length - lookback] };
|
||||
|
||||
stdDevInd.Run(inputs, options, outputs);
|
||||
var tResult = outputs[0];
|
||||
|
||||
ValidationHelper.VerifyData(qOutput, tResult, lookback);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region MathNet Validation
|
||||
|
||||
[Fact]
|
||||
public void StdDev_Matches_MathNet_Sample()
|
||||
{
|
||||
int period = 20;
|
||||
var stdDev = new StdDev(period, isPopulation: false);
|
||||
var popStdDev = new StdDev(period, isPopulation: true);
|
||||
|
||||
var quotes = _data.SkenderQuotes.ToList();
|
||||
double[] input = quotes.Select(q => (double)q.Close).ToArray();
|
||||
double[] input = _testData.RawData.ToArray();
|
||||
|
||||
for (int i = 0; i < input.Length; i++)
|
||||
{
|
||||
var val = stdDev.Update(new TValue(DateTime.UtcNow, input[i]));
|
||||
var popVal = popStdDev.Update(new TValue(DateTime.UtcNow, input[i]));
|
||||
|
||||
if (i >= input.Length - 100)
|
||||
if (i >= period - 1)
|
||||
{
|
||||
var window = input[(i - period + 1)..(i + 1)];
|
||||
double expected = Statistics.StandardDeviation(window);
|
||||
double expectedPop = Statistics.PopulationStandardDeviation(window);
|
||||
|
||||
double expected = MathNet.Numerics.Statistics.Statistics.StandardDeviation(window);
|
||||
Assert.Equal(expected, val.Value, ValidationHelper.DefaultTolerance);
|
||||
Assert.Equal(expectedPop, popVal.Value, ValidationHelper.DefaultTolerance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDev_Matches_MathNet_Population()
|
||||
{
|
||||
int period = 20;
|
||||
var stdDev = new StdDev(period, isPopulation: true);
|
||||
double[] input = _testData.RawData.ToArray();
|
||||
|
||||
for (int i = 0; i < input.Length; i++)
|
||||
{
|
||||
var val = stdDev.Update(new TValue(DateTime.UtcNow, input[i]));
|
||||
|
||||
if (i >= period - 1)
|
||||
{
|
||||
var window = input[(i - period + 1)..(i + 1)];
|
||||
double expected = MathNet.Numerics.Statistics.Statistics.PopulationStandardDeviation(window);
|
||||
Assert.Equal(expected, val.Value, ValidationHelper.DefaultTolerance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Comprehensive Tests
|
||||
|
||||
[Fact]
|
||||
public void StdDev_AllModes_ProduceIdenticalResults()
|
||||
{
|
||||
// Critical validation: All 3 API modes must produce identical results
|
||||
int[] periods = { 5, 10, 20, 50 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Test both population and sample
|
||||
foreach (bool isPopulation in new[] { true, false })
|
||||
{
|
||||
// 1. Batch Mode (TSeries)
|
||||
var batchStdDev = new StdDev(period, isPopulation);
|
||||
var batchResult = batchStdDev.Update(_testData.Data);
|
||||
|
||||
// 2. Span Mode
|
||||
double[] sourceData = _testData.RawData.ToArray();
|
||||
double[] spanOutput = new double[sourceData.Length];
|
||||
StdDev.Batch(sourceData.AsSpan(), spanOutput.AsSpan(), period, isPopulation);
|
||||
|
||||
// 3. Streaming Mode
|
||||
var streamingStdDev = new StdDev(period, isPopulation);
|
||||
var streamingResults = new List<double>();
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
streamingResults.Add(streamingStdDev.Update(item).Value);
|
||||
}
|
||||
|
||||
// Compare all modes (allow 1e-8 tolerance for accumulated floating-point errors)
|
||||
for (int i = 0; i < _testData.Data.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult[i].Value, spanOutput[i], 1e-8);
|
||||
Assert.Equal(batchResult[i].Value, streamingResults[i], 1e-8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDev_Matches_SqrtVariance()
|
||||
{
|
||||
// StdDev = Sqrt(Variance)
|
||||
// Validate this relationship holds
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
foreach (bool isPopulation in new[] { true, false })
|
||||
{
|
||||
var stdDev = new StdDev(period, isPopulation);
|
||||
var variance = new Variance(period, isPopulation);
|
||||
|
||||
for (int i = 0; i < _testData.Data.Count; i++)
|
||||
{
|
||||
var input = _testData.Data[i];
|
||||
var s = stdDev.Update(input);
|
||||
var v = variance.Update(input);
|
||||
|
||||
double expected = Math.Sqrt(Math.Max(0, v.Value));
|
||||
Assert.Equal(expected, s.Value, 1e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDev_FlatLine_ProducesZero()
|
||||
{
|
||||
// Flat price should produce zero standard deviation
|
||||
var stdDev = new StdDev(10);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
stdDev.Update(new TValue(DateTime.UtcNow, 100));
|
||||
}
|
||||
|
||||
// After sufficient warmup, flat line should produce StdDev ≈ 0
|
||||
Assert.True(Math.Abs(stdDev.Last.Value) < 1e-10,
|
||||
$"Expected StdDev ≈ 0 for flat line, got {stdDev.Last.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDev_LargeDataset_MaintainsPrecision()
|
||||
{
|
||||
// Test with large dataset to ensure no drift
|
||||
int period = 20;
|
||||
var stdDev = new StdDev(period, isPopulation: true);
|
||||
var variance = new Variance(period, isPopulation: true);
|
||||
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
var bars = gbm.Fetch(10000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Close.Count; i++)
|
||||
{
|
||||
var input = bars.Close[i];
|
||||
var s = stdDev.Update(input);
|
||||
var v = variance.Update(input);
|
||||
|
||||
// Every 1000th point, verify precision
|
||||
if (i % 1000 == 0 && i > period)
|
||||
{
|
||||
double expected = Math.Sqrt(Math.Max(0, v.Value));
|
||||
Assert.Equal(expected, s.Value, 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDev_PopulationVsSample_Difference()
|
||||
{
|
||||
// Population and Sample StdDev should differ
|
||||
int period = 10;
|
||||
var popStdDev = new StdDev(period, isPopulation: true);
|
||||
var sampStdDev = new StdDev(period, isPopulation: false);
|
||||
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.3, seed: 123);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars.Close)
|
||||
{
|
||||
popStdDev.Update(bar);
|
||||
sampStdDev.Update(bar);
|
||||
}
|
||||
|
||||
// Sample StdDev should be larger than Population StdDev (divides by N-1 instead of N)
|
||||
Assert.True(sampStdDev.IsHot && popStdDev.IsHot);
|
||||
Assert.True(sampStdDev.Last.Value > popStdDev.Last.Value,
|
||||
$"Sample StdDev ({sampStdDev.Last.Value}) should be > Population StdDev ({popStdDev.Last.Value})");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDev_BatchSpan_HandlesNaN_InMiddle()
|
||||
{
|
||||
double[] data = new double[100];
|
||||
var gbm = new GBM(startPrice: 100, seed: 42);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
data[i] = gbm.Next().Close;
|
||||
}
|
||||
|
||||
// Insert NaN in the middle
|
||||
data[50] = double.NaN;
|
||||
|
||||
double[] output = new double[100];
|
||||
StdDev.Batch(data.AsSpan(), output.AsSpan(), 10);
|
||||
|
||||
// All outputs should be finite
|
||||
foreach (var value in output)
|
||||
{
|
||||
Assert.True(double.IsFinite(value), $"Expected finite value, got {value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDev_Convergence_AfterWarmup()
|
||||
{
|
||||
// After warmup period, indicator should be "hot"
|
||||
int[] periods = { 5, 10, 20, 50 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var stdDev = new StdDev(period);
|
||||
|
||||
Assert.False(stdDev.IsHot);
|
||||
|
||||
// Feed period number of bars
|
||||
for (int i = 0; i < period - 1; i++)
|
||||
{
|
||||
stdDev.Update(_testData.Data[i]);
|
||||
Assert.False(stdDev.IsHot);
|
||||
}
|
||||
|
||||
stdDev.Update(_testData.Data[period - 1]);
|
||||
Assert.True(stdDev.IsHot);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDev_DifferentPeriods_ProduceDifferentSensitivity()
|
||||
{
|
||||
// Shorter periods should be more sensitive to price changes
|
||||
var stdDev5 = new StdDev(5);
|
||||
var stdDev20 = new StdDev(20);
|
||||
var stdDev50 = new StdDev(50);
|
||||
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.3, seed: 123);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars.Close)
|
||||
{
|
||||
stdDev5.Update(bar);
|
||||
stdDev20.Update(bar);
|
||||
stdDev50.Update(bar);
|
||||
}
|
||||
|
||||
// All periods should produce finite numeric results
|
||||
Assert.True(double.IsFinite(stdDev5.Last.Value));
|
||||
Assert.True(double.IsFinite(stdDev20.Last.Value));
|
||||
Assert.True(double.IsFinite(stdDev50.Last.Value));
|
||||
|
||||
// All should be hot
|
||||
Assert.True(stdDev5.IsHot && stdDev20.IsHot && stdDev50.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDev_EdgeCase_Period2()
|
||||
{
|
||||
// Period=2 is minimum (constructor throws on period=1)
|
||||
var stdDev = new StdDev(2);
|
||||
|
||||
stdDev.Update(new TValue(DateTime.UtcNow, 100));
|
||||
stdDev.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
// Two identical values should produce StdDev = 0
|
||||
Assert.Equal(0, stdDev.Last.Value, 1e-10);
|
||||
|
||||
stdDev.Update(new TValue(DateTime.UtcNow, 110));
|
||||
// 100, 110: mean = 105, deviations = -5, 5, squared = 25, 25, sum = 50
|
||||
// Population variance = 50/2 = 25, StdDev = 5
|
||||
// Sample variance = 50/1 = 50, StdDev = 7.071...
|
||||
|
||||
// Default is sample (isPopulation=false)
|
||||
Assert.Equal(Math.Sqrt(50), stdDev.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class SumIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void SumIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new SumIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("SUM - Rolling Sum", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new SumIndicator();
|
||||
|
||||
Assert.Equal(0, SumIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumIndicator_ShortName_IncludesPeriodAndSource()
|
||||
{
|
||||
var indicator = new SumIndicator { Period = 20 };
|
||||
|
||||
Assert.Contains("SUM", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumIndicator_Initialize_CreatesInternalSum()
|
||||
{
|
||||
var indicator = new SumIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new SumIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have a value
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new SumIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new SumIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
double firstValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
double secondValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(firstValue));
|
||||
Assert.True(double.IsFinite(secondValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumIndicator_MultipleUpdates_ProducesCorrectSumSequence()
|
||||
{
|
||||
var indicator = new SumIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 10, 20, 30, 40, 50 };
|
||||
|
||||
foreach (var close in closes)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
// All values should be finite
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
|
||||
}
|
||||
|
||||
// Last SUM(3) should be sum of last 3 values: 30 + 40 + 50 = 120
|
||||
double lastSum = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(120.0, lastSum, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new SumIndicator { Period = 5, Source = source };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
|
||||
$"Source {source} should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumIndicator_CalculatesRollingSum()
|
||||
{
|
||||
var indicator = new SumIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add bars with known close prices: 10, 20, 30, 40
|
||||
indicator.HistoricalData.AddBar(now, 10, 10, 10, 10);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
Assert.Equal(10.0, indicator.LinesSeries[0].GetValue(0), 1e-10); // Sum = 10
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 20, 20, 20, 20);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
Assert.Equal(30.0, indicator.LinesSeries[0].GetValue(0), 1e-10); // Sum = 10+20 = 30
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(2), 30, 30, 30, 30);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
Assert.Equal(60.0, indicator.LinesSeries[0].GetValue(0), 1e-10); // Sum = 10+20+30 = 60
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(3), 40, 40, 40, 40);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
Assert.Equal(90.0, indicator.LinesSeries[0].GetValue(0), 1e-10); // Sum = 20+30+40 = 90 (10 dropped)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new SumIndicator { Period = 50 };
|
||||
|
||||
Assert.Equal(50, indicator.Period);
|
||||
|
||||
indicator.Period = 100;
|
||||
Assert.Equal(100, indicator.Period);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class SumIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 0, minimum: 1, maximum: 10000)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Sum? _sum;
|
||||
private readonly LineSeries? _series;
|
||||
private string? _sourceName;
|
||||
private Func<IHistoryItem, double>? _priceSelector;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"SUM({Period}):{_sourceName}";
|
||||
|
||||
public SumIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "SUM - Rolling Sum";
|
||||
Description = "Rolling Sum with Kahan-Babuška summation for numerical stability";
|
||||
_series = new(name: "SUM", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
_sourceName = Source.ToString();
|
||||
_sum = new Sum(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
bool isNew = args.IsNewBar();
|
||||
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _sum!.Update(new TValue(item.TimeLeft.Ticks, _priceSelector!(item)), isNew).Value;
|
||||
_series!.SetValue(value, _sum.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,613 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class SumTests
|
||||
{
|
||||
[Fact]
|
||||
public void Sum_Constructor_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Sum(0));
|
||||
Assert.Throws<ArgumentException>(() => new Sum(-1));
|
||||
|
||||
var sum = new Sum(10);
|
||||
Assert.NotNull(sum);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_Calc_ReturnsValue()
|
||||
{
|
||||
var sum = new Sum(10);
|
||||
|
||||
Assert.Equal(0, sum.Last.Value);
|
||||
|
||||
TValue result = sum.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
Assert.True(result.Value > 0);
|
||||
Assert.Equal(result.Value, sum.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_FirstValue_ReturnsItself()
|
||||
{
|
||||
var sum = new Sum(10);
|
||||
|
||||
TValue result = sum.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
Assert.Equal(100.0, result.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var sum = new Sum(10);
|
||||
|
||||
sum.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
|
||||
double value1 = sum.Last.Value;
|
||||
|
||||
sum.Update(new TValue(DateTime.UtcNow, 200), isNew: true);
|
||||
double value2 = sum.Last.Value;
|
||||
|
||||
Assert.NotEqual(value1, value2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_Calc_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var sum = new Sum(10);
|
||||
|
||||
sum.Update(new TValue(DateTime.UtcNow, 100));
|
||||
sum.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
|
||||
double beforeUpdate = sum.Last.Value;
|
||||
|
||||
sum.Update(new TValue(DateTime.UtcNow, 120), isNew: false);
|
||||
double afterUpdate = sum.Last.Value;
|
||||
|
||||
Assert.NotEqual(beforeUpdate, afterUpdate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_Reset_ClearsState()
|
||||
{
|
||||
var sum = new Sum(10);
|
||||
|
||||
sum.Update(new TValue(DateTime.UtcNow, 100));
|
||||
sum.Update(new TValue(DateTime.UtcNow, 105));
|
||||
double valueBefore = sum.Last.Value;
|
||||
|
||||
sum.Reset();
|
||||
|
||||
Assert.Equal(0, sum.Last.Value);
|
||||
Assert.False(sum.IsHot);
|
||||
|
||||
sum.Update(new TValue(DateTime.UtcNow, 50));
|
||||
Assert.NotEqual(0, sum.Last.Value);
|
||||
Assert.NotEqual(valueBefore, sum.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_Properties_Accessible()
|
||||
{
|
||||
var sum = new Sum(10);
|
||||
|
||||
Assert.Equal(0, sum.Last.Value);
|
||||
Assert.False(sum.IsHot);
|
||||
|
||||
sum.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
Assert.NotEqual(0, sum.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_IsHot_BecomesTrueWhenBufferFull()
|
||||
{
|
||||
var sum = new Sum(5);
|
||||
|
||||
Assert.False(sum.IsHot);
|
||||
|
||||
for (int i = 1; i <= 4; i++)
|
||||
{
|
||||
sum.Update(new TValue(DateTime.UtcNow, i * 10));
|
||||
Assert.False(sum.IsHot);
|
||||
}
|
||||
|
||||
sum.Update(new TValue(DateTime.UtcNow, 50));
|
||||
Assert.True(sum.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_CalculatesCorrectSum()
|
||||
{
|
||||
var sum = new Sum(5);
|
||||
|
||||
sum.Update(new TValue(DateTime.UtcNow, 10));
|
||||
Assert.Equal(10.0, sum.Last.Value, 1e-10); // 10
|
||||
|
||||
sum.Update(new TValue(DateTime.UtcNow, 20));
|
||||
Assert.Equal(30.0, sum.Last.Value, 1e-10); // 10+20
|
||||
|
||||
sum.Update(new TValue(DateTime.UtcNow, 30));
|
||||
Assert.Equal(60.0, sum.Last.Value, 1e-10); // 10+20+30
|
||||
|
||||
sum.Update(new TValue(DateTime.UtcNow, 40));
|
||||
Assert.Equal(100.0, sum.Last.Value, 1e-10); // 10+20+30+40
|
||||
|
||||
sum.Update(new TValue(DateTime.UtcNow, 50));
|
||||
Assert.Equal(150.0, sum.Last.Value, 1e-10); // 10+20+30+40+50
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_SlidingWindow_Works()
|
||||
{
|
||||
var sum = new Sum(3);
|
||||
|
||||
sum.Update(new TValue(DateTime.UtcNow, 10));
|
||||
sum.Update(new TValue(DateTime.UtcNow, 20));
|
||||
sum.Update(new TValue(DateTime.UtcNow, 30));
|
||||
Assert.Equal(60.0, sum.Last.Value, 1e-10); // 10+20+30
|
||||
|
||||
sum.Update(new TValue(DateTime.UtcNow, 40));
|
||||
Assert.Equal(90.0, sum.Last.Value, 1e-10); // 20+30+40
|
||||
|
||||
sum.Update(new TValue(DateTime.UtcNow, 50));
|
||||
Assert.Equal(120.0, sum.Last.Value, 1e-10); // 30+40+50
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var sum = new Sum(5);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
|
||||
// Feed 10 new values
|
||||
TValue tenthInput = default;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
tenthInput = new TValue(bar.Time, bar.Close);
|
||||
sum.Update(tenthInput, isNew: true);
|
||||
}
|
||||
|
||||
// Remember state after 10 values
|
||||
double stateAfterTen = sum.Last.Value;
|
||||
|
||||
// Generate 9 corrections with isNew=false (different values)
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: false);
|
||||
sum.Update(new TValue(bar.Time, bar.Close), isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered 10th input again with isNew=false
|
||||
TValue finalResult = sum.Update(tenthInput, isNew: false);
|
||||
|
||||
// State should match the original state after 10 values
|
||||
Assert.Equal(stateAfterTen, finalResult.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_BatchCalc_MatchesIterativeCalc()
|
||||
{
|
||||
var sumIterative = new Sum(10);
|
||||
var sumBatch = new Sum(10);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
|
||||
var series = new TSeries();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
Assert.True(series.Count > 0);
|
||||
|
||||
// Calculate iteratively
|
||||
var iterativeResults = new TSeries();
|
||||
foreach (var item in series)
|
||||
{
|
||||
iterativeResults.Add(sumIterative.Update(item));
|
||||
}
|
||||
|
||||
// Calculate batch
|
||||
var batchResults = sumBatch.Update(series);
|
||||
|
||||
// Compare
|
||||
Assert.Equal(iterativeResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < iterativeResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(iterativeResults[i].Value, batchResults[i].Value, 1e-10);
|
||||
Assert.Equal(iterativeResults[i].Time, batchResults[i].Time);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var sum = new Sum(5);
|
||||
|
||||
sum.Update(new TValue(DateTime.UtcNow, 100));
|
||||
sum.Update(new TValue(DateTime.UtcNow, 110));
|
||||
|
||||
var resultAfterNaN = sum.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
Assert.True(double.IsFinite(resultAfterNaN.Value));
|
||||
Assert.NotEqual(0, resultAfterNaN.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var sum = new Sum(5);
|
||||
|
||||
sum.Update(new TValue(DateTime.UtcNow, 100));
|
||||
sum.Update(new TValue(DateTime.UtcNow, 110));
|
||||
|
||||
var resultAfterPosInf = sum.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(resultAfterPosInf.Value));
|
||||
|
||||
var resultAfterNegInf = sum.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
|
||||
Assert.True(double.IsFinite(resultAfterNegInf.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_MultipleNaN_ContinuesWithLastValid()
|
||||
{
|
||||
var sum = new Sum(5);
|
||||
|
||||
sum.Update(new TValue(DateTime.UtcNow, 100));
|
||||
sum.Update(new TValue(DateTime.UtcNow, 110));
|
||||
sum.Update(new TValue(DateTime.UtcNow, 120));
|
||||
|
||||
var r1 = sum.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
var r2 = sum.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
var r3 = sum.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
Assert.True(double.IsFinite(r1.Value));
|
||||
Assert.True(double.IsFinite(r2.Value));
|
||||
Assert.True(double.IsFinite(r3.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_BatchCalc_HandlesNaN()
|
||||
{
|
||||
var sum = new Sum(5);
|
||||
|
||||
var series = new TSeries();
|
||||
series.Add(DateTime.UtcNow.Ticks, 100);
|
||||
series.Add(DateTime.UtcNow.Ticks + 1, 110);
|
||||
series.Add(DateTime.UtcNow.Ticks + 2, double.NaN);
|
||||
series.Add(DateTime.UtcNow.Ticks + 3, 120);
|
||||
series.Add(DateTime.UtcNow.Ticks + 4, double.PositiveInfinity);
|
||||
series.Add(DateTime.UtcNow.Ticks + 5, 130);
|
||||
|
||||
var results = sum.Update(series);
|
||||
|
||||
foreach (var result in results)
|
||||
{
|
||||
Assert.True(double.IsFinite(result.Value), $"Expected finite value but got {result.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_Reset_ClearsLastValidValue()
|
||||
{
|
||||
var sum = new Sum(5);
|
||||
|
||||
sum.Update(new TValue(DateTime.UtcNow, 100));
|
||||
sum.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
sum.Reset();
|
||||
|
||||
var result = sum.Update(new TValue(DateTime.UtcNow, 50));
|
||||
Assert.Equal(50.0, result.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_StaticBatch_Works()
|
||||
{
|
||||
var series = new TSeries();
|
||||
series.Add(DateTime.UtcNow.Ticks, 10);
|
||||
series.Add(DateTime.UtcNow.Ticks + 1, 20);
|
||||
series.Add(DateTime.UtcNow.Ticks + 2, 30);
|
||||
series.Add(DateTime.UtcNow.Ticks + 3, 40);
|
||||
series.Add(DateTime.UtcNow.Ticks + 4, 50);
|
||||
|
||||
var results = Sum.Batch(series, 3);
|
||||
|
||||
Assert.Equal(5, results.Count);
|
||||
// Sum(3) for last value: 30+40+50 = 120
|
||||
Assert.Equal(120.0, results.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_FlatLine_ReturnsSameValue()
|
||||
{
|
||||
var sum = new Sum(10);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
sum.Update(new TValue(DateTime.UtcNow, 100));
|
||||
}
|
||||
|
||||
// Sum of 10 values of 100 = 1000
|
||||
Assert.Equal(1000.0, sum.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
// ============== Span API Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void Sum_SpanBatch_ValidatesInput()
|
||||
{
|
||||
double[] source = [1, 2, 3, 4, 5];
|
||||
double[] output = new double[5];
|
||||
double[] wrongSizeOutput = new double[3];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Sum.Batch(source.AsSpan(), output.AsSpan(), 0));
|
||||
Assert.Throws<ArgumentException>(() => Sum.Batch(source.AsSpan(), output.AsSpan(), -1));
|
||||
Assert.Throws<ArgumentException>(() => Sum.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_SpanBatch_MatchesTSeriesBatch()
|
||||
{
|
||||
var series = new TSeries();
|
||||
double[] source = new double[100];
|
||||
double[] output = new double[100];
|
||||
|
||||
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);
|
||||
source[i] = bar.Close;
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
var tseriesResult = Sum.Batch(series, 10);
|
||||
Sum.Batch(source.AsSpan(), output.AsSpan(), 10);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
Assert.Equal(tseriesResult[i].Value, output[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_SpanBatch_CalculatesCorrectly()
|
||||
{
|
||||
double[] source = [10, 20, 30, 40, 50];
|
||||
double[] output = new double[5];
|
||||
|
||||
Sum.Batch(source.AsSpan(), output.AsSpan(), 3);
|
||||
|
||||
Assert.Equal(10.0, output[0], 1e-10); // 10
|
||||
Assert.Equal(30.0, output[1], 1e-10); // 10+20
|
||||
Assert.Equal(60.0, output[2], 1e-10); // 10+20+30
|
||||
Assert.Equal(90.0, output[3], 1e-10); // 20+30+40
|
||||
Assert.Equal(120.0, output[4], 1e-10); // 30+40+50
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_SpanBatch_ZeroAllocation()
|
||||
{
|
||||
double[] source = new double[10000];
|
||||
double[] output = new double[10000];
|
||||
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
source[i] = gbm.Next().Close;
|
||||
|
||||
Sum.Batch(source.AsSpan(), output.AsSpan(), 100);
|
||||
|
||||
Assert.True(double.IsFinite(output[^1]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_SpanBatch_HandlesNaN()
|
||||
{
|
||||
double[] source = [100, 110, double.NaN, 120, 130];
|
||||
double[] output = new double[5];
|
||||
|
||||
Sum.Batch(source.AsSpan(), output.AsSpan(), 3);
|
||||
|
||||
foreach (var val in output)
|
||||
{
|
||||
Assert.True(double.IsFinite(val), $"Expected finite value but got {val}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_AllModes_ProduceSameResult()
|
||||
{
|
||||
// Arrange
|
||||
int period = 10;
|
||||
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 = Sum.Batch(series, period);
|
||||
double expected = batchSeries.Last.Value;
|
||||
|
||||
// 2. Span Mode
|
||||
var tValues = series.Values.ToArray();
|
||||
var spanInput = new ReadOnlySpan<double>(tValues);
|
||||
var spanOutput = new double[tValues.Length];
|
||||
Sum.Batch(spanInput, spanOutput, period);
|
||||
double spanResult = spanOutput[^1];
|
||||
|
||||
// 3. Streaming Mode
|
||||
var streamingInd = new Sum(period);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
streamingInd.Update(series[i]);
|
||||
}
|
||||
double streamingResult = streamingInd.Last.Value;
|
||||
|
||||
// 4. Eventing Mode
|
||||
var pubSource = new TSeries();
|
||||
var eventingInd = new Sum(pubSource, period);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
pubSource.Add(series[i]);
|
||||
}
|
||||
double eventingResult = eventingInd.Last.Value;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, spanResult, precision: 9);
|
||||
Assert.Equal(expected, streamingResult, precision: 9);
|
||||
Assert.Equal(expected, eventingResult, precision: 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_Chainability_Works()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var sum = new Sum(source, 10);
|
||||
|
||||
source.Add(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.Equal(100, sum.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_WarmupPeriod_IsSetCorrectly()
|
||||
{
|
||||
var sum = new Sum(10);
|
||||
Assert.Equal(10, sum.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_Prime_SetsStateCorrectly()
|
||||
{
|
||||
var sum = new Sum(5);
|
||||
double[] history = [10, 20, 30, 40, 50]; // Sum = 150
|
||||
|
||||
sum.Prime(history);
|
||||
|
||||
Assert.True(sum.IsHot);
|
||||
Assert.Equal(150.0, sum.Last.Value, 1e-10);
|
||||
|
||||
// Verify it continues correctly with sliding window
|
||||
sum.Update(new TValue(DateTime.UtcNow, 60)); // 20+30+40+50+60 = 200
|
||||
Assert.Equal(200.0, sum.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_Prime_WithInsufficientHistory_IsNotHot()
|
||||
{
|
||||
var sum = new Sum(10);
|
||||
double[] history = [10, 20, 30, 40, 50];
|
||||
|
||||
sum.Prime(history);
|
||||
|
||||
Assert.False(sum.IsHot);
|
||||
Assert.Equal(150.0, sum.Last.Value, 1e-10); // Sum of what we have
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_Prime_HandlesNaN_InHistory()
|
||||
{
|
||||
var sum = new Sum(3);
|
||||
double[] history = [10, 20, double.NaN, 40];
|
||||
// Values used: 10, 20, 20 (NaN replaced), 40
|
||||
// Final window (3): 20, 20, 40 = 80
|
||||
|
||||
sum.Prime(history);
|
||||
|
||||
Assert.True(sum.IsHot);
|
||||
Assert.True(double.IsFinite(sum.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_Calculate_ReturnsCorrectResultsAndHotIndicator()
|
||||
{
|
||||
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
|
||||
|
||||
var (results, indicator) = Sum.Calculate(series, 5);
|
||||
|
||||
// Check results
|
||||
Assert.Equal(10, results.Count);
|
||||
Assert.Equal(150.0, results[4].Value, 1e-10); // Sum(10..50) = 150
|
||||
Assert.Equal(400.0, results.Last.Value, 1e-10); // Sum(60..100) = 400
|
||||
|
||||
// Check indicator state
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.Equal(400.0, indicator.Last.Value, 1e-10);
|
||||
Assert.Equal(5, indicator.WarmupPeriod);
|
||||
|
||||
// Verify indicator continues correctly
|
||||
indicator.Update(new TValue(DateTime.UtcNow, 110));
|
||||
// Sum now = 70+80+90+100+110 = 450
|
||||
Assert.Equal(450.0, indicator.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_NumericalStability_LargeDataset()
|
||||
{
|
||||
// Test that Sum remains stable over a large number of values
|
||||
var sum = new Sum(100);
|
||||
|
||||
for (int i = 1; i <= 100000; i++)
|
||||
{
|
||||
sum.Update(new TValue(DateTime.UtcNow, 1.0));
|
||||
}
|
||||
|
||||
// Sum of 100 values of 1.0 = 100
|
||||
Assert.Equal(100.0, sum.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_NumericalStability_VaryingMagnitudes()
|
||||
{
|
||||
// Test with values of wildly different magnitudes
|
||||
var sum = new Sum(4);
|
||||
|
||||
sum.Update(new TValue(DateTime.UtcNow, 1e10));
|
||||
sum.Update(new TValue(DateTime.UtcNow, 1.0));
|
||||
sum.Update(new TValue(DateTime.UtcNow, 1e-10));
|
||||
sum.Update(new TValue(DateTime.UtcNow, 1e10));
|
||||
|
||||
// Kahan-Babuška should handle this accurately
|
||||
double expected = 1e10 + 1.0 + 1e-10 + 1e10;
|
||||
Assert.Equal(expected, sum.Last.Value, 1e-5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_KahanBabuska_BetterThanNaive()
|
||||
{
|
||||
// Test case that would cause precision loss with naive summation
|
||||
var sum = new Sum(1000);
|
||||
|
||||
// Add a large value followed by many small values
|
||||
sum.Update(new TValue(DateTime.UtcNow, 1e15));
|
||||
|
||||
for (int i = 0; i < 999; i++)
|
||||
{
|
||||
sum.Update(new TValue(DateTime.UtcNow, 1.0));
|
||||
}
|
||||
|
||||
// With Kahan-Babuška, the small values should not be lost
|
||||
// Naive sum would lose precision
|
||||
double expected = 1e15 + 999.0;
|
||||
double actual = sum.Last.Value;
|
||||
|
||||
// Should be very close to expected
|
||||
double relativeError = Math.Abs(actual - expected) / expected;
|
||||
Assert.True(relativeError < 1e-14, $"Relative error {relativeError} too large");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_Period1_ReturnsInput()
|
||||
{
|
||||
var sum = new Sum(1);
|
||||
|
||||
sum.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.Equal(100.0, sum.Last.Value, 1e-10);
|
||||
|
||||
sum.Update(new TValue(DateTime.UtcNow, 200));
|
||||
Assert.Equal(200.0, sum.Last.Value, 1e-10);
|
||||
|
||||
sum.Update(new TValue(DateTime.UtcNow, 150));
|
||||
Assert.Equal(150.0, sum.Last.Value, 1e-10);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
using Xunit.Abstractions;
|
||||
using TALib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for Sum (Summation with Kahan-Babuška algorithm).
|
||||
/// Validates against TA-Lib SUM function and mathematical calculations.
|
||||
/// </summary>
|
||||
public sealed class SumValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _testData;
|
||||
private readonly ITestOutputHelper _output;
|
||||
private bool _disposed;
|
||||
|
||||
public SumValidationTests(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_Talib_Batch()
|
||||
{
|
||||
int[] periods = [5, 10, 20, 50, 100];
|
||||
double[] tData = _testData.RawData.ToArray();
|
||||
double[] output = new double[tData.Length];
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var sum = new Sum(period);
|
||||
var qResult = sum.Update(_testData.Data);
|
||||
|
||||
var retCode = Functions.Sum<double>(tData, 0..^0, output, out var outRange, period);
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.SumLookback(period);
|
||||
|
||||
ValidationHelper.VerifyData(qResult, output, outRange, lookback, ValidationHelper.DefaultVerificationCount, ValidationHelper.TalibTolerance);
|
||||
}
|
||||
_output.WriteLine("Sum Batch(TSeries) validated against TA-Lib");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Talib_Streaming()
|
||||
{
|
||||
int[] periods = [5, 10, 20, 50, 100];
|
||||
double[] tData = _testData.RawData.ToArray();
|
||||
double[] output = new double[tData.Length];
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var sum = new Sum(period);
|
||||
var qResults = new List<double>();
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
qResults.Add(sum.Update(item).Value);
|
||||
}
|
||||
|
||||
var retCode = Functions.Sum<double>(tData, 0..^0, output, out var outRange, period);
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.SumLookback(period);
|
||||
|
||||
ValidationHelper.VerifyData(qResults, output, outRange, lookback, ValidationHelper.DefaultVerificationCount, ValidationHelper.TalibTolerance);
|
||||
}
|
||||
_output.WriteLine("Sum Streaming validated against TA-Lib");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Talib_Span()
|
||||
{
|
||||
int[] periods = [5, 10, 20, 50, 100];
|
||||
double[] sourceData = _testData.RawData.ToArray();
|
||||
double[] tOutput = new double[sourceData.Length];
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
double[] qOutput = new double[sourceData.Length];
|
||||
Sum.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period);
|
||||
|
||||
var retCode = Functions.Sum<double>(sourceData, 0..^0, tOutput, out var outRange, period);
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.SumLookback(period);
|
||||
|
||||
ValidationHelper.VerifyData(qOutput, tOutput, outRange, lookback, ValidationHelper.DefaultVerificationCount, ValidationHelper.TalibTolerance);
|
||||
}
|
||||
_output.WriteLine("Sum Span validated against TA-Lib");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_MathematicalCorrectness_Batch()
|
||||
{
|
||||
int period = 10;
|
||||
var sum = new Sum(period);
|
||||
var qResult = sum.Update(_testData.Data);
|
||||
|
||||
// Calculate expected sum manually using naive approach
|
||||
var rawData = _testData.RawData.ToArray();
|
||||
|
||||
for (int i = 0; i < rawData.Length; i++)
|
||||
{
|
||||
double expectedSum = 0;
|
||||
int startIdx = Math.Max(0, i - period + 1);
|
||||
for (int j = startIdx; j <= i; j++)
|
||||
{
|
||||
expectedSum += rawData[j];
|
||||
}
|
||||
|
||||
double qValue = qResult[i].Value;
|
||||
|
||||
Assert.True(
|
||||
Math.Abs(qValue - expectedSum) <= ValidationHelper.DefaultTolerance,
|
||||
$"Mismatch at index {i}: QuanTAlib={qValue:G17}, Expected={expectedSum:G17}");
|
||||
}
|
||||
|
||||
_output.WriteLine("Sum Batch validated against manual calculation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_MathematicalCorrectness_Streaming()
|
||||
{
|
||||
int period = 10;
|
||||
var sum = new Sum(period);
|
||||
var qResults = new List<double>();
|
||||
var rawData = _testData.RawData.ToArray();
|
||||
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
qResults.Add(sum.Update(item).Value);
|
||||
}
|
||||
|
||||
for (int i = 0; i < rawData.Length; i++)
|
||||
{
|
||||
double expectedSum = 0;
|
||||
int startIdx = Math.Max(0, i - period + 1);
|
||||
for (int j = startIdx; j <= i; j++)
|
||||
{
|
||||
expectedSum += rawData[j];
|
||||
}
|
||||
|
||||
Assert.True(
|
||||
Math.Abs(qResults[i] - expectedSum) <= ValidationHelper.DefaultTolerance,
|
||||
$"Mismatch at index {i}: QuanTAlib={qResults[i]:G17}, Expected={expectedSum:G17}");
|
||||
}
|
||||
|
||||
_output.WriteLine("Sum Streaming validated against manual calculation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_MathematicalCorrectness_Span()
|
||||
{
|
||||
int period = 10;
|
||||
var sourceData = _testData.RawData.ToArray();
|
||||
var qOutput = new double[sourceData.Length];
|
||||
|
||||
Sum.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period);
|
||||
|
||||
for (int i = 0; i < sourceData.Length; i++)
|
||||
{
|
||||
double expectedSum = 0;
|
||||
int startIdx = Math.Max(0, i - period + 1);
|
||||
for (int j = startIdx; j <= i; j++)
|
||||
{
|
||||
expectedSum += sourceData[j];
|
||||
}
|
||||
|
||||
Assert.True(
|
||||
Math.Abs(qOutput[i] - expectedSum) <= ValidationHelper.DefaultTolerance,
|
||||
$"Mismatch at index {i}: QuanTAlib={qOutput[i]:G17}, Expected={expectedSum:G17}");
|
||||
}
|
||||
|
||||
_output.WriteLine("Sum Span validated against manual calculation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_KahanBabuska_Stability_LargeValues()
|
||||
{
|
||||
// Test numerical stability with large values
|
||||
var sum = new Sum(1000);
|
||||
double[] largeValues = new double[1000];
|
||||
double baseValue = 1e10;
|
||||
|
||||
for (int i = 0; i < largeValues.Length; i++)
|
||||
{
|
||||
largeValues[i] = baseValue + i;
|
||||
}
|
||||
|
||||
// Calculate sum
|
||||
foreach (var val in largeValues)
|
||||
{
|
||||
sum.Update(new TValue(DateTime.UtcNow, val));
|
||||
}
|
||||
|
||||
// Expected: sum of 1e10, 1e10+1, ..., 1e10+999
|
||||
// = 1000 * 1e10 + sum of 0,1,2,...,999
|
||||
// = 1e13 + 999*1000/2 = 1e13 + 499500
|
||||
double expectedSum = 1000 * baseValue + 499500.0;
|
||||
|
||||
Assert.Equal(expectedSum, sum.Last.Value, 1e-4);
|
||||
_output.WriteLine($"Sum Kahan-Babuška stability test passed: {sum.Last.Value:G17}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_KahanBabuska_Stability_SmallDifferences()
|
||||
{
|
||||
// Test with values that have small differences (challenges precision)
|
||||
var sum = new Sum(10000);
|
||||
double[] values = new double[10000];
|
||||
double baseValue = 1e8;
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
values[i] = baseValue + (i % 2 == 0 ? 0.1 : -0.1);
|
||||
}
|
||||
|
||||
foreach (var val in values)
|
||||
{
|
||||
sum.Update(new TValue(DateTime.UtcNow, val));
|
||||
}
|
||||
|
||||
// With alternating +0.1 and -0.1, the sum is 10000 * baseValue
|
||||
// Use tolerance scaled to magnitude (relative error ~1e-12 is excellent for 1e12 scale)
|
||||
double expectedSum = 10000 * baseValue;
|
||||
Assert.Equal(expectedSum, sum.Last.Value, 1.0);
|
||||
_output.WriteLine($"Sum small differences test passed: {sum.Last.Value:G17}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_AgainstNaiveSum_ShortSequence()
|
||||
{
|
||||
double[] values = [100, 200, 150, 175, 125, 180, 160, 140, 190, 170];
|
||||
var sum = new Sum(5);
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
sum.Update(new TValue(DateTime.UtcNow, values[i]));
|
||||
|
||||
// Calculate naive sum for the window
|
||||
double naiveSum = 0;
|
||||
int startIdx = Math.Max(0, i - 4); // Period = 5, so window starts 4 back
|
||||
for (int j = startIdx; j <= i; j++)
|
||||
{
|
||||
naiveSum += values[j];
|
||||
}
|
||||
|
||||
Assert.Equal(naiveSum, sum.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
_output.WriteLine("Sum validated against naive sum for short sequence");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_KnownSequence_ArithmeticProgression()
|
||||
{
|
||||
// Arithmetic progression: 1, 2, 3, ..., n with period 5
|
||||
// Sum at index i = sum of values from max(0, i-4) to i
|
||||
|
||||
var sum = new Sum(5);
|
||||
|
||||
for (int n = 1; n <= 100; n++)
|
||||
{
|
||||
sum.Update(new TValue(DateTime.UtcNow, n));
|
||||
|
||||
// Calculate expected sum for window [n-4, n] (or [1, n] if n < 5)
|
||||
int windowStart = Math.Max(1, n - 4);
|
||||
// Sum of windowStart to n = (n - windowStart + 1) * (windowStart + n) / 2
|
||||
double expected = (n - windowStart + 1) * (double)(windowStart + n) / 2;
|
||||
|
||||
Assert.Equal(expected, sum.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
_output.WriteLine("Sum validated for arithmetic progression");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ConstantSequence()
|
||||
{
|
||||
// Sum of constant sequence with period n should be n * constant
|
||||
double constant = 42.5;
|
||||
int period = 100;
|
||||
var sum = new Sum(period);
|
||||
|
||||
for (int i = 0; i < 10000; i++)
|
||||
{
|
||||
sum.Update(new TValue(DateTime.UtcNow, constant));
|
||||
|
||||
int windowSize = Math.Min(i + 1, period);
|
||||
double expected = windowSize * constant;
|
||||
Assert.Equal(expected, sum.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
_output.WriteLine("Sum validated for constant sequence");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_AllModes_Consistency()
|
||||
{
|
||||
int period = 20;
|
||||
var sourceData = _testData.RawData.ToArray();
|
||||
|
||||
// Mode 1: TSeries Batch
|
||||
var sum1 = new Sum(period);
|
||||
var batchResult = sum1.Update(_testData.Data);
|
||||
|
||||
// Mode 2: Streaming
|
||||
var sum2 = new Sum(period);
|
||||
var streamingResults = new List<double>();
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
streamingResults.Add(sum2.Update(item).Value);
|
||||
}
|
||||
|
||||
// Mode 3: Span
|
||||
var spanOutput = new double[sourceData.Length];
|
||||
Sum.Batch(sourceData.AsSpan(), spanOutput.AsSpan(), period);
|
||||
|
||||
// Compare all three
|
||||
for (int i = 0; i < sourceData.Length; i++)
|
||||
{
|
||||
double batchVal = batchResult[i].Value;
|
||||
double streamVal = streamingResults[i];
|
||||
double spanVal = spanOutput[i];
|
||||
|
||||
Assert.Equal(batchVal, streamVal, 1e-8);
|
||||
Assert.Equal(batchVal, spanVal, 1e-8);
|
||||
}
|
||||
|
||||
_output.WriteLine("All Sum calculation modes produce consistent results");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_KahanBabuska_AdversarialInput()
|
||||
{
|
||||
// This is the classic adversarial case for naive summation
|
||||
// Large positive followed by many small negatives that should cancel
|
||||
var sum = new Sum(1001);
|
||||
|
||||
sum.Update(new TValue(DateTime.UtcNow, 1e16));
|
||||
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
sum.Update(new TValue(DateTime.UtcNow, -1e13));
|
||||
}
|
||||
|
||||
// Expected: 1e16 - 1000 * 1e13 = 1e16 - 1e16 = 0
|
||||
double expected = 1e16 - 1000 * 1e13;
|
||||
|
||||
// With Kahan-Babuška, this should be accurate
|
||||
// Naive sum would have significant error
|
||||
Assert.Equal(expected, sum.Last.Value, 1e2);
|
||||
_output.WriteLine($"Adversarial input test: Expected={expected:G17}, Actual={sum.Last.Value:G17}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Tulip_Batch()
|
||||
{
|
||||
int[] periods = [5, 10, 20, 50, 100];
|
||||
double[] tData = _testData.RawData.ToArray();
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var sum = new Sum(period);
|
||||
var qResult = sum.Update(_testData.Data);
|
||||
|
||||
var sumIndicator = Tulip.Indicators.sum;
|
||||
double[][] inputs = [tData];
|
||||
double[] options = [period];
|
||||
int lookback = period - 1;
|
||||
double[][] outputs = [new double[tData.Length - lookback]];
|
||||
|
||||
sumIndicator.Run(inputs, options, outputs);
|
||||
var tResult = outputs[0];
|
||||
|
||||
ValidationHelper.VerifyData(qResult, tResult, lookback, ValidationHelper.DefaultVerificationCount, ValidationHelper.TulipTolerance);
|
||||
}
|
||||
_output.WriteLine("Sum Batch validated against Tulip");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Sum: Summation over a rolling window using Kahan-Babuška algorithm
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Sum calculates the sum of the last n values using the Kahan-Babuška summation
|
||||
/// algorithm (also known as "improved Kahan") for maximum numerical precision.
|
||||
///
|
||||
/// Kahan-Babuška fixes second-order rounding errors that classic Kahan misses:
|
||||
/// - Tracks two compensation layers: primary (c) and secondary (cc)
|
||||
/// - Captures rounding losses that Kahan itself introduces
|
||||
/// - Error bounded closer to machine epsilon, even for pathological sequences
|
||||
///
|
||||
/// Algorithm:
|
||||
/// For each value x:
|
||||
/// y = x - c
|
||||
/// t = sum + y
|
||||
/// c = (t - sum) - y
|
||||
/// sum = t
|
||||
/// // compensate the compensation
|
||||
/// z = c - cc
|
||||
/// tt = sum + z
|
||||
/// cc = (tt - sum) - z
|
||||
/// sum = tt
|
||||
///
|
||||
/// Key Features:
|
||||
/// - Near machine-epsilon accuracy for streaming summation
|
||||
/// - Handles adversarial inputs (wildly different magnitudes)
|
||||
/// - O(1) time complexity per update with RingBuffer
|
||||
/// - Branch-free core algorithm
|
||||
///
|
||||
/// IsHot:
|
||||
/// Becomes true when the buffer is full (period samples processed).
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Sum : AbstractBase
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly RingBuffer _buffer;
|
||||
private readonly TValuePublishedHandler _handler;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
double Sum, // Accumulated sum
|
||||
double C, // First-order compensation
|
||||
double Cc, // Second-order compensation
|
||||
double LastInput,
|
||||
double LastValidValue,
|
||||
int TickCount
|
||||
);
|
||||
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
|
||||
private const int ResyncInterval = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// Creates Sum with specified period.
|
||||
/// </summary>
|
||||
/// <param name="period">Number of values to sum (must be > 0)</param>
|
||||
public Sum(int period)
|
||||
{
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
|
||||
_period = period;
|
||||
_buffer = new RingBuffer(period);
|
||||
Name = $"Sum({period})";
|
||||
WarmupPeriod = period;
|
||||
_handler = Handle;
|
||||
}
|
||||
|
||||
public Sum(ITValuePublisher source, int period) : this(period)
|
||||
{
|
||||
source.Pub += _handler;
|
||||
}
|
||||
|
||||
public Sum(TSeries source, int period) : this(period)
|
||||
{
|
||||
Prime(source.Values);
|
||||
if (source.Count > 0)
|
||||
{
|
||||
Last = new TValue(source.LastTime, Last.Value);
|
||||
}
|
||||
source.Pub += _handler;
|
||||
}
|
||||
|
||||
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Mode B: Streaming (Stateful)
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/// <summary>
|
||||
/// True if the Sum has enough data to produce valid results.
|
||||
/// Sum is "hot" when the buffer is full (has received at least 'period' values).
|
||||
/// </summary>
|
||||
public override bool IsHot => _buffer.IsFull;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Kahan-Babuška Core Operations
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/// <summary>
|
||||
/// Adds a value using Kahan-Babuška summation.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void KahanBabuskaAdd(double x)
|
||||
{
|
||||
// Primary Kahan step
|
||||
double y = x - _state.C;
|
||||
double t = _state.Sum + y;
|
||||
_state.C = (t - _state.Sum) - y;
|
||||
_state.Sum = t;
|
||||
|
||||
// Secondary compensation (Babuška improvement)
|
||||
double z = _state.C - _state.Cc;
|
||||
double tt = _state.Sum + z;
|
||||
_state.Cc = (tt - _state.Sum) - z;
|
||||
_state.Sum = tt;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts a value using Kahan-Babuška summation.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void KahanBabuskaSubtract(double x)
|
||||
{
|
||||
KahanBabuskaAdd(-x);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recalculates the sum from scratch using Kahan-Babuška.
|
||||
/// Used for periodic resync to prevent drift.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void RecalculateSum()
|
||||
{
|
||||
_state.Sum = 0;
|
||||
_state.C = 0;
|
||||
_state.Cc = 0;
|
||||
|
||||
var bufferSpan = _buffer.GetSpan();
|
||||
for (int i = 0; i < bufferSpan.Length; i++)
|
||||
{
|
||||
KahanBabuskaAdd(bufferSpan[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Mode C: Priming (The Bridge)
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the indicator state using the provided history.
|
||||
/// </summary>
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
if (source.Length == 0) return;
|
||||
|
||||
// Reset state
|
||||
_buffer.Clear();
|
||||
_state = default;
|
||||
_p_state = default;
|
||||
|
||||
int warmupLength = Math.Min(source.Length, WarmupPeriod);
|
||||
int startIndex = source.Length - warmupLength;
|
||||
|
||||
// Seed LastValidValue
|
||||
_state.LastValidValue = double.NaN;
|
||||
for (int i = startIndex - 1; i >= 0; i--)
|
||||
{
|
||||
if (double.IsFinite(source[i]))
|
||||
{
|
||||
_state.LastValidValue = source[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (double.IsNaN(_state.LastValidValue))
|
||||
{
|
||||
for (int i = startIndex; i < source.Length; i++)
|
||||
{
|
||||
if (double.IsFinite(source[i]))
|
||||
{
|
||||
_state.LastValidValue = source[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Feed the buffer and calculate sum
|
||||
for (int i = startIndex; i < source.Length; i++)
|
||||
{
|
||||
double val = GetValidValue(source[i]);
|
||||
_buffer.Add(val);
|
||||
KahanBabuskaAdd(val);
|
||||
_state.LastInput = val;
|
||||
}
|
||||
|
||||
Last = new TValue(DateTime.MinValue, _state.Sum);
|
||||
_p_state = _state;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double GetValidValue(double input)
|
||||
{
|
||||
if (double.IsFinite(input))
|
||||
{
|
||||
_state.LastValidValue = input;
|
||||
return input;
|
||||
}
|
||||
return _state.LastValidValue;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void UpdateState(double val)
|
||||
{
|
||||
if (_buffer.Count == _buffer.Capacity)
|
||||
{
|
||||
KahanBabuskaSubtract(_buffer.Oldest);
|
||||
}
|
||||
|
||||
_buffer.Add(val);
|
||||
KahanBabuskaAdd(val);
|
||||
|
||||
_state.TickCount++;
|
||||
if (_buffer.IsFull && _state.TickCount >= ResyncInterval)
|
||||
{
|
||||
_state.TickCount = 0;
|
||||
RecalculateSum();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_p_state = _state;
|
||||
|
||||
double val = GetValidValue(input.Value);
|
||||
UpdateState(val);
|
||||
_state.LastInput = val;
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = _p_state;
|
||||
|
||||
double val = GetValidValue(input.Value);
|
||||
|
||||
// Recalculate: remove old bar value, add new correction value
|
||||
if (_buffer.Count == _buffer.Capacity)
|
||||
{
|
||||
KahanBabuskaSubtract(_buffer.Oldest);
|
||||
}
|
||||
|
||||
// Replace the newest value in buffer
|
||||
if (_buffer.Count > 0)
|
||||
{
|
||||
// We need to subtract the value that was added and add the new one
|
||||
// Since we restored state, we add directly
|
||||
_buffer.UpdateNewest(val);
|
||||
RecalculateSum(); // Ensure accuracy after correction
|
||||
}
|
||||
else
|
||||
{
|
||||
_buffer.Add(val);
|
||||
KahanBabuskaAdd(val);
|
||||
}
|
||||
}
|
||||
|
||||
Last = new TValue(input.Time, _state.Sum);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0) return [];
|
||||
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
|
||||
Batch(source.Values, vSpan, _period);
|
||||
source.Times.CopyTo(tSpan);
|
||||
|
||||
Prime(source.Values);
|
||||
|
||||
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Mode A: Batch (Stateless)
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Sum for the entire series using a new instance.
|
||||
/// </summary>
|
||||
public static TSeries Batch(TSeries source, int period)
|
||||
{
|
||||
var sum = new Sum(period);
|
||||
return sum.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Sum in-place using Kahan-Babuška summation.
|
||||
/// Zero-allocation method for maximum performance.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period)
|
||||
{
|
||||
if (source.Length != output.Length)
|
||||
throw new ArgumentException("Source and output must have the same length", nameof(output));
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
|
||||
int len = source.Length;
|
||||
if (len == 0) return;
|
||||
|
||||
CalculateScalarCore(source, output, period);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a batch calculation and returns a "Hot" Sum instance.
|
||||
/// </summary>
|
||||
public static (TSeries Results, Sum Indicator) Calculate(TSeries source, int period)
|
||||
{
|
||||
var sum = new Sum(period);
|
||||
TSeries results = sum.Update(source);
|
||||
return (results, sum);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void CalculateScalarCore(ReadOnlySpan<double> source, Span<double> output, int period)
|
||||
{
|
||||
int len = source.Length;
|
||||
|
||||
const int StackAllocThreshold = 256;
|
||||
Span<double> buffer = period <= StackAllocThreshold
|
||||
? stackalloc double[period]
|
||||
: new double[period];
|
||||
|
||||
// Kahan-Babuška state
|
||||
double sum = 0;
|
||||
double c = 0; // First-order compensation
|
||||
double cc = 0; // Second-order compensation
|
||||
double lastValid = double.NaN;
|
||||
|
||||
// Find first valid value
|
||||
for (int k = 0; k < len; k++)
|
||||
{
|
||||
if (double.IsFinite(source[k]))
|
||||
{
|
||||
lastValid = source[k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int bufferIndex = 0;
|
||||
int tickCount = 0;
|
||||
|
||||
// Warmup phase
|
||||
int warmupEnd = Math.Min(period, len);
|
||||
for (int i = 0; i < warmupEnd; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (double.IsFinite(val))
|
||||
lastValid = val;
|
||||
else
|
||||
val = lastValid;
|
||||
|
||||
// Kahan-Babuška add
|
||||
double y = val - c;
|
||||
double t = sum + y;
|
||||
c = (t - sum) - y;
|
||||
sum = t;
|
||||
|
||||
double z = c - cc;
|
||||
double tt = sum + z;
|
||||
cc = (tt - sum) - z;
|
||||
sum = tt;
|
||||
|
||||
buffer[i] = val;
|
||||
output[i] = sum;
|
||||
}
|
||||
|
||||
// Main phase with sliding window
|
||||
for (int i = period; i < len; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (double.IsFinite(val))
|
||||
lastValid = val;
|
||||
else
|
||||
val = lastValid;
|
||||
|
||||
double oldVal = buffer[bufferIndex];
|
||||
|
||||
// Kahan-Babuška subtract old value
|
||||
double yS = -oldVal - c;
|
||||
double tS = sum + yS;
|
||||
c = (tS - sum) - yS;
|
||||
sum = tS;
|
||||
|
||||
double zS = c - cc;
|
||||
double ttS = sum + zS;
|
||||
cc = (ttS - sum) - zS;
|
||||
sum = ttS;
|
||||
|
||||
// Kahan-Babuška add new value
|
||||
double yA = val - c;
|
||||
double tA = sum + yA;
|
||||
c = (tA - sum) - yA;
|
||||
sum = tA;
|
||||
|
||||
double zA = c - cc;
|
||||
double ttA = sum + zA;
|
||||
cc = (ttA - sum) - zA;
|
||||
sum = ttA;
|
||||
|
||||
buffer[bufferIndex] = val;
|
||||
bufferIndex++;
|
||||
if (bufferIndex >= period)
|
||||
bufferIndex = 0;
|
||||
|
||||
output[i] = sum;
|
||||
|
||||
// Periodic resync for long sequences
|
||||
tickCount++;
|
||||
if (tickCount >= ResyncInterval)
|
||||
{
|
||||
tickCount = 0;
|
||||
sum = 0;
|
||||
c = 0;
|
||||
cc = 0;
|
||||
for (int k = 0; k < period; k++)
|
||||
{
|
||||
double bVal = buffer[k];
|
||||
double yR = bVal - c;
|
||||
double tR = sum + yR;
|
||||
c = (tR - sum) - yR;
|
||||
sum = tR;
|
||||
|
||||
double zR = c - cc;
|
||||
double ttR = sum + zR;
|
||||
cc = (ttR - sum) - zR;
|
||||
sum = ttR;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the Sum state.
|
||||
/// </summary>
|
||||
public override void Reset()
|
||||
{
|
||||
_buffer.Clear();
|
||||
_state = default;
|
||||
_p_state = default;
|
||||
Last = default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
# Sum: Summation with Kahan-Babuška Algorithm
|
||||
|
||||
> "The naive approach to summation assumes all digits matter equally. They don't. When you add 1e-10 to 1e10, that small value vanishes into the rounding noise. Kahan-Babuška tracks what got lost and adds it back later. It's bookkeeping for bits that would otherwise slip through the cracks."
|
||||
|
||||
The Sum indicator calculates a rolling window summation using the Kahan-Babuška algorithm (also known as "improved Kahan" or "second-order compensated summation") for maximum numerical precision. This approach captures rounding errors that even classic Kahan summation misses, making it suitable for numerical libraries, statistics, and trading applications where precision matters.
|
||||
|
||||
## Historical Context
|
||||
|
||||
The Kahan summation algorithm was introduced by William Kahan in 1965 to reduce the numerical error in the total obtained by adding a sequence of finite-precision floating-point numbers. The Kahan-Babuška variant extends this by tracking a second level of compensation, capturing errors introduced during the compensation step itself.
|
||||
|
||||
Standard rolling sum implementations use naive addition/subtraction, which accumulates floating-point rounding errors over time. After millions of ticks with values spanning multiple orders of magnitude, these errors can become significant. The Kahan-Babuška approach keeps error bounded near machine epsilon regardless of sequence length.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
### The Precision Problem
|
||||
|
||||
Consider summing values that span many orders of magnitude:
|
||||
|
||||
```csharp
|
||||
double sum = 1e15;
|
||||
sum += 1.0; // The 1.0 is lost due to limited precision
|
||||
```
|
||||
|
||||
With 64-bit doubles, adding a small value to a large sum can result in the small value being completely absorbed into rounding error. In a sliding window sum, this happens on both addition and subtraction, compounding the problem.
|
||||
|
||||
### Kahan-Babuška Solution
|
||||
|
||||
The algorithm maintains three running values:
|
||||
|
||||
- `sum`: The accumulated sum
|
||||
- `c`: First-order compensation (captures primary rounding error)
|
||||
- `cc`: Second-order compensation (captures error of the error)
|
||||
|
||||
For each value `x` to add:
|
||||
|
||||
```csharp
|
||||
// Primary Kahan step
|
||||
double y = x - c;
|
||||
double t = sum + y;
|
||||
c = (t - sum) - y;
|
||||
sum = t;
|
||||
|
||||
// Secondary compensation (Babuška improvement)
|
||||
double z = c - cc;
|
||||
double tt = sum + z;
|
||||
cc = (tt - sum) - z;
|
||||
sum = tt;
|
||||
```
|
||||
|
||||
This formulation:
|
||||
|
||||
1. Computes the lost precision from each addition
|
||||
2. Tracks the lost precision from computing the lost precision
|
||||
3. Reintegrates both error terms into subsequent operations
|
||||
|
||||
### Sliding Window Complexity
|
||||
|
||||
For a rolling window sum, values must be both added (new) and subtracted (old). The Kahan-Babuška approach handles subtraction identically by negating the value before applying the algorithm. Periodic resync (recalculating from buffer contents) prevents long-term drift.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### 1. Kahan Summation (First Order)
|
||||
|
||||
For each value $x$ to add to sum $S$:
|
||||
|
||||
$$y = x - c$$
|
||||
$$t = S + y$$
|
||||
$$c = (t - S) - y$$
|
||||
$$S = t$$
|
||||
|
||||
Where $c$ captures the low-order bits lost in the addition.
|
||||
|
||||
### 2. Babuška Extension (Second Order)
|
||||
|
||||
After the primary step, compensate the compensation:
|
||||
|
||||
$$z = c - cc$$
|
||||
$$tt = S + z$$
|
||||
$$cc = (tt - S) - z$$
|
||||
$$S = tt$$
|
||||
|
||||
### 3. Error Bound
|
||||
|
||||
Standard summation error: $O(n \cdot \epsilon)$
|
||||
|
||||
Kahan summation error: $O(\sqrt{n} \cdot \epsilon)$
|
||||
|
||||
Kahan-Babuška error: Approaches machine epsilon $\epsilon$ regardless of $n$
|
||||
|
||||
Where $\epsilon \approx 2.2 \times 10^{-16}$ for 64-bit doubles.
|
||||
|
||||
## Performance Profile
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **Throughput** | ~15 ns/bar | ~2× Kahan, ~3× naive |
|
||||
| **Allocations** | 0 | Zero-allocation in hot paths |
|
||||
| **Complexity** | O(1) | Constant time per update with RingBuffer |
|
||||
| **Accuracy** | 10 | Near machine-epsilon precision |
|
||||
| **Memory** | O(n) | RingBuffer stores period values |
|
||||
|
||||
## Validation
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **TA-Lib** | ✅ | Matches `TA_SUM` function |
|
||||
| **Tulip** | ✅ | Matches `ti.sum` indicator |
|
||||
| **Mathematical** | ✅ | Validated against naive calculation |
|
||||
|
||||
## Use Cases
|
||||
|
||||
1. **Rolling Statistics**: Foundation for moving averages, standard deviation
|
||||
2. **Volume Analysis**: Summing volume over periods
|
||||
3. **Price Totals**: Accumulating price changes
|
||||
4. **High-Precision Finance**: Where rounding errors have monetary impact
|
||||
|
||||
## API Usage
|
||||
|
||||
### Streaming Mode
|
||||
|
||||
```csharp
|
||||
var sum = new Sum(period: 20);
|
||||
foreach (var price in prices)
|
||||
{
|
||||
var result = sum.Update(new TValue(DateTime.UtcNow, price));
|
||||
Console.WriteLine($"Rolling Sum: {result.Value}");
|
||||
}
|
||||
```
|
||||
|
||||
### Batch Mode
|
||||
|
||||
```csharp
|
||||
var series = new TSeries();
|
||||
// ... populate series ...
|
||||
var results = Sum.Batch(series, period: 20);
|
||||
```
|
||||
|
||||
### Span Mode (Zero Allocation)
|
||||
|
||||
```csharp
|
||||
double[] input = new double[1000];
|
||||
double[] output = new double[1000];
|
||||
// ... populate input ...
|
||||
Sum.Batch(input.AsSpan(), output.AsSpan(), period: 20);
|
||||
```
|
||||
|
||||
### Event-Driven Mode
|
||||
|
||||
```csharp
|
||||
var source = new TSeries();
|
||||
var sum = new Sum(source, period: 20);
|
||||
// Sum automatically updates when source publishes
|
||||
source.Add(new TValue(DateTime.UtcNow, 100.0));
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Overkill for Simple Cases**: If your values are all similar magnitude and sequence length is short, naive summation is faster and sufficient.
|
||||
|
||||
2. **Period Selection**: A very large period means more values in the buffer and more memory usage.
|
||||
|
||||
3. **Resync Frequency**: The default resync interval (1000 updates) provides a good balance between performance and drift prevention. Adjust if needed for extreme precision requirements.
|
||||
|
||||
4. **Not a Substitute for Decimal**: For financial applications requiring exact decimal representation, use `decimal` type. Kahan-Babuška improves floating-point accuracy but doesn't eliminate floating-point representation limitations.
|
||||
|
||||
## When to Use Kahan-Babuška
|
||||
|
||||
**Use it when:**
|
||||
|
||||
- Writing a numerical or statistics library
|
||||
- Inputs span many orders of magnitude
|
||||
- Correctness matters more than raw throughput
|
||||
- Long-running streaming calculations
|
||||
|
||||
**Skip it when:**
|
||||
|
||||
- Values are similar magnitude
|
||||
- Sequence length is bounded and small
|
||||
- Maximum throughput is critical
|
||||
- Using `decimal` type instead
|
||||
@@ -1,7 +1,6 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
namespace QuanTAlib.Quantower.Tests;
|
||||
|
||||
public class VarianceIndicatorTests
|
||||
{
|
||||
@@ -12,21 +11,29 @@ public class VarianceIndicatorTests
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.False(indicator.IsPopulation);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("Variance - Rolling Variance", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new VarianceIndicator { Period = 20 };
|
||||
var indicator = new VarianceIndicator();
|
||||
|
||||
Assert.Equal(0, VarianceIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_ShortName_IncludesPeriod()
|
||||
{
|
||||
var indicator = new VarianceIndicator { Period = 14 };
|
||||
|
||||
Assert.True(indicator.ShortName.Contains("Variance", StringComparison.Ordinal));
|
||||
Assert.True(indicator.ShortName.Contains("14", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -39,7 +46,6 @@ public class VarianceIndicatorTests
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("Variance", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -50,19 +56,175 @@ public class VarianceIndicatorTests
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
// Need enough bars for Period
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have a value
|
||||
double variance = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(variance));
|
||||
[Fact]
|
||||
public void VarianceIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new VarianceIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new VarianceIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Should not throw an exception
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
// Assert that the indicator still exists (method completed without exception)
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_MultipleUpdates_ProducesCorrectSequence()
|
||||
{
|
||||
var indicator = new VarianceIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 102, 105, 103, 107, 110 };
|
||||
|
||||
foreach (var close in closes)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
// All values should be finite
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new VarianceIndicator { Period = 5, Source = source };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
|
||||
$"Source {source} should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new VarianceIndicator { Period = 10 };
|
||||
|
||||
Assert.Equal(10, indicator.Period);
|
||||
|
||||
indicator.Period = 20;
|
||||
Assert.Equal(20, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_IsPopulation_CanBeChanged()
|
||||
{
|
||||
var indicator = new VarianceIndicator { IsPopulation = false };
|
||||
|
||||
Assert.False(indicator.IsPopulation);
|
||||
|
||||
indicator.IsPopulation = true;
|
||||
Assert.True(indicator.IsPopulation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_Source_CanBeChanged()
|
||||
{
|
||||
var indicator = new VarianceIndicator { Source = SourceType.Close };
|
||||
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
|
||||
indicator.Source = SourceType.Open;
|
||||
Assert.Equal(SourceType.Open, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_ShowColdValues_CanBeChanged()
|
||||
{
|
||||
var indicator = new VarianceIndicator { ShowColdValues = true };
|
||||
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_ShortName_UpdatesWhenPeriodChanges()
|
||||
{
|
||||
var indicator = new VarianceIndicator { Period = 10 };
|
||||
string initialName = indicator.ShortName;
|
||||
|
||||
Assert.True(initialName.Contains("10", StringComparison.Ordinal));
|
||||
|
||||
indicator.Period = 20;
|
||||
string updatedName = indicator.ShortName;
|
||||
|
||||
Assert.True(updatedName.Contains("20", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_ProcessUpdate_IgnoresNonBarUpdates()
|
||||
{
|
||||
var indicator = new VarianceIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process historical bar first
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Process other update reasons - should not throw
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
// Assert that the indicator still exists (method completed without exception)
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceIndicator_LineSeries_HasCorrectProperties()
|
||||
{
|
||||
var indicator = new VarianceIndicator { Period = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var lineSeries = indicator.LinesSeries[0];
|
||||
|
||||
Assert.Equal("Variance", lineSeries.Name);
|
||||
Assert.Equal(2, lineSeries.Width);
|
||||
Assert.Equal(LineStyle.Solid, lineSeries.Style);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,9 @@ public sealed class VarianceIndicator : Indicator, IWatchlistIndicator
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
if (args.Reason != UpdateReason.NewBar && args.Reason != UpdateReason.HistoricalBar)
|
||||
return;
|
||||
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector!(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class VarianceTests
|
||||
@@ -180,38 +179,71 @@ public class VarianceTests
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Assert.Equal(tseriesResult[i].Value, output[i], 1e-10);
|
||||
Assert.Equal(tseriesResult[i].Value, output[i], precision: 10);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Fact]
|
||||
public void Batch_SimdPath_Triggered()
|
||||
{
|
||||
// Create dataset that should trigger SIMD (clean, large)
|
||||
int count = 300;
|
||||
var data = new double[count];
|
||||
var output = new double[count];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
data[i] = Math.Sin(i * 0.1); // Clean finite values
|
||||
}
|
||||
|
||||
Variance.Batch(data, output, 10);
|
||||
|
||||
// Should complete without error and produce finite values
|
||||
for (int i = 9; i < count; i++) // Start from period-1
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]));
|
||||
Assert.True(output[i] >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculation_KnownValues()
|
||||
public void Batch_LargeDataset_ForceSimd()
|
||||
{
|
||||
// Data: 2, 4, 4, 4, 5, 5, 7, 9
|
||||
// Mean: 5
|
||||
// Deviations: -3, -1, -1, -1, 0, 0, 2, 4
|
||||
// Sq Devs: 9, 1, 1, 1, 0, 0, 4, 16
|
||||
// Sum Sq Devs: 32
|
||||
// Population Variance (N=8): 32 / 8 = 4
|
||||
// Sample Variance (N-1=7): 32 / 7 = 4.571428...
|
||||
// Force SIMD path with large clean dataset
|
||||
int count = 1000;
|
||||
var data = new double[count];
|
||||
var output = new double[count];
|
||||
|
||||
var data = new double[] { 2, 4, 4, 4, 5, 5, 7, 9 };
|
||||
|
||||
// Test Population Variance
|
||||
var popVar = new Variance(8, isPopulation: true);
|
||||
foreach (var val in data)
|
||||
// Generate clean, finite data
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
popVar.Update(new TValue(DateTime.UtcNow, val));
|
||||
data[i] = Math.Sin(i * 0.01) + 10; // Clean finite values, positive
|
||||
}
|
||||
Assert.Equal(4.0, popVar.Last.Value, precision: 6);
|
||||
|
||||
// Test Sample Variance
|
||||
var sampVar = new Variance(8, isPopulation: false);
|
||||
foreach (var val in data)
|
||||
Variance.Batch(data, output, 10);
|
||||
|
||||
// Verify results are finite and reasonable
|
||||
for (int i = 9; i < count; i++)
|
||||
{
|
||||
sampVar.Update(new TValue(DateTime.UtcNow, val));
|
||||
Assert.True(double.IsFinite(output[i]));
|
||||
Assert.True(output[i] >= 0);
|
||||
}
|
||||
|
||||
// Verify against streaming calculation for correctness
|
||||
var variance = new Variance(10);
|
||||
double[] streamingOutput = new double[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
streamingOutput[i] = variance.Update(new TValue(DateTime.UtcNow, data[i])).Value;
|
||||
}
|
||||
|
||||
// Compare last 100 values
|
||||
for (int i = count - 100; i < count; i++)
|
||||
{
|
||||
Assert.Equal(streamingOutput[i], output[i], precision: 10);
|
||||
}
|
||||
Assert.Equal(32.0 / 7.0, sampVar.Last.Value, precision: 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -322,22 +354,6 @@ public class VarianceTests
|
||||
Assert.True(double.IsNaN(result));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resync_DoesNotDrift()
|
||||
{
|
||||
// Run for > 1000 updates to trigger Resync
|
||||
var variance = new Variance(10);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
|
||||
for (int i = 0; i < 1100; i++)
|
||||
{
|
||||
variance.Update(new TValue(DateTime.UtcNow, gbm.Next().Close));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(variance.Last.Value));
|
||||
Assert.True(variance.Last.Value >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_LargeDataset_Simd()
|
||||
{
|
||||
@@ -350,6 +366,8 @@ public class VarianceTests
|
||||
|
||||
// Batch calculation
|
||||
var batchResult = Variance.Calculate(series, 10);
|
||||
Assert.True(double.IsFinite(batchResult.Last.Value));
|
||||
Assert.True(batchResult.Last.Value >= 0);
|
||||
|
||||
// Verify last value against streaming
|
||||
var variance = new Variance(10);
|
||||
@@ -361,4 +379,348 @@ public class VarianceTests
|
||||
|
||||
Assert.Equal(lastStreaming, batchResult.Last.Value, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_Method_Works()
|
||||
{
|
||||
var variance = new Variance(5);
|
||||
double[] primeData = [10, 20, 30, 40, 50];
|
||||
|
||||
variance.Prime(primeData.AsSpan());
|
||||
|
||||
Assert.True(variance.IsHot);
|
||||
Assert.Equal(250.0, variance.Last.Value, precision: 6); // Variance of [10,20,30,40,50] = 1000/4 = 250
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_WithInsufficientData()
|
||||
{
|
||||
var variance = new Variance(5);
|
||||
double[] primeData = [10, 20]; // Less than period
|
||||
|
||||
variance.Prime(primeData.AsSpan());
|
||||
|
||||
Assert.False(variance.IsHot);
|
||||
Assert.Equal(50.0, variance.Last.Value, precision: 6); // Variance of [10,20] = 50/1 = 50
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_WithEmptySpan()
|
||||
{
|
||||
var variance = new Variance(5);
|
||||
|
||||
variance.Prime(ReadOnlySpan<double>.Empty);
|
||||
|
||||
Assert.False(variance.IsHot);
|
||||
Assert.Equal(0, variance.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TSeries_ReturnsCorrectSeries()
|
||||
{
|
||||
var source = new TSeries();
|
||||
source.Add(DateTime.UtcNow.Ticks, 10);
|
||||
source.Add(DateTime.UtcNow.Ticks + 1, 20);
|
||||
source.Add(DateTime.UtcNow.Ticks + 2, 30);
|
||||
source.Add(DateTime.UtcNow.Ticks + 3, 40);
|
||||
source.Add(DateTime.UtcNow.Ticks + 4, 50);
|
||||
|
||||
var variance = new Variance(3);
|
||||
var result = variance.Update(source);
|
||||
|
||||
Assert.Equal(5, result.Count);
|
||||
Assert.Equal(source.Times[0], result.Times[0]);
|
||||
Assert.Equal(source.Times[4], result.Times[4]);
|
||||
|
||||
// Check variance values
|
||||
Assert.Equal(0, result[0].Value); // N=1, no variance
|
||||
Assert.Equal(50.0, result[1].Value, precision: 6); // Var([10,20]) = 50
|
||||
Assert.Equal(100.0, result[2].Value, precision: 6); // Var([10,20,30]) = 200/2 = 100
|
||||
Assert.Equal(100.0, result[3].Value, precision: 6); // Var([20,30,40]) = 200/2 = 100
|
||||
Assert.Equal(100.0, result[4].Value, precision: 6); // Var([30,40,50]) = 200/2 = 100
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TSeries_EmptySource()
|
||||
{
|
||||
var variance = new Variance(5);
|
||||
var result = variance.Update(new TSeries());
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TSeries_PrimesState()
|
||||
{
|
||||
var source = new TSeries();
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
source.Add(DateTime.UtcNow.Ticks + i, i * 10);
|
||||
}
|
||||
|
||||
var variance = new Variance(5);
|
||||
variance.Update(source);
|
||||
|
||||
// Should be primed with last 5 values
|
||||
Assert.True(variance.IsHot);
|
||||
|
||||
// Add one more value and check it continues correctly
|
||||
var newValue = variance.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.True(double.IsFinite(newValue.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_StaticMethod_Works()
|
||||
{
|
||||
var source = new TSeries();
|
||||
source.Add(DateTime.UtcNow.Ticks, 10);
|
||||
source.Add(DateTime.UtcNow.Ticks + 1, 20);
|
||||
source.Add(DateTime.UtcNow.Ticks + 2, 30);
|
||||
|
||||
var result = Variance.Calculate(source, 3); // Sample variance by default
|
||||
|
||||
Assert.Equal(3, result.Count);
|
||||
Assert.Equal(100.0, result.Last.Value, precision: 6); // Sample variance: 200/2 = 100
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_StaticMethod_PopulationVariance()
|
||||
{
|
||||
var source = new TSeries();
|
||||
source.Add(DateTime.UtcNow.Ticks, 10);
|
||||
source.Add(DateTime.UtcNow.Ticks + 1, 20);
|
||||
source.Add(DateTime.UtcNow.Ticks + 2, 30);
|
||||
|
||||
var result = Variance.Calculate(source, 3, isPopulation: true);
|
||||
|
||||
Assert.Equal(3, result.Count);
|
||||
Assert.Equal(66.666666, result.Last.Value, precision: 5); // Population variance: 200/3 ≈ 66.67
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_WithNaNInData()
|
||||
{
|
||||
double[] source = [10, 20, double.NaN, 40, 50];
|
||||
double[] output = new double[5];
|
||||
|
||||
Variance.Batch(source, output, 3);
|
||||
|
||||
// Should handle NaN gracefully
|
||||
foreach (var val in output)
|
||||
{
|
||||
Assert.True(double.IsFinite(val) || double.IsNaN(val));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_PeriodEqualsTwo()
|
||||
{
|
||||
double[] source = [10, 20, 30, 40];
|
||||
double[] output = new double[4];
|
||||
|
||||
Variance.Batch(source, output, 2);
|
||||
|
||||
Assert.Equal(0, output[0]); // N=1
|
||||
Assert.Equal(50, output[1]); // Var([10,20]) = 50
|
||||
Assert.Equal(50, output[2]); // Var([20,30]) = 50
|
||||
Assert.Equal(50, output[3]); // Var([30,40]) = 50
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_VeryLargePeriod()
|
||||
{
|
||||
double[] source = [10, 20, 30, 40, 50];
|
||||
double[] output = new double[5];
|
||||
|
||||
Variance.Batch(source, output, 5);
|
||||
|
||||
Assert.Equal(0, output[0]); // N=1, variance undefined
|
||||
Assert.Equal(50, output[1]); // Var([10,20]) = 50
|
||||
Assert.Equal(100, output[2]); // Var([10,20,30]) = 200/2 = 100
|
||||
Assert.Equal(500.0/3.0, output[3], precision: 6); // Var([10,20,30,40]) = 500/3 ≈ 166.67
|
||||
Assert.Equal(250, output[4], precision: 6); // Var([10,20,30,40,50]) = 1000/4 = 250
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_SingleElement()
|
||||
{
|
||||
double[] source = [42];
|
||||
double[] output = new double[1];
|
||||
|
||||
Variance.Batch(source, output, 2);
|
||||
|
||||
Assert.Equal(0, output[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_ConstantValues_ZeroVariance()
|
||||
{
|
||||
double[] source = [5, 5, 5, 5, 5];
|
||||
double[] output = new double[5];
|
||||
|
||||
Variance.Batch(source, output, 3);
|
||||
|
||||
Assert.Equal(0, output[0]);
|
||||
Assert.Equal(0, output[1]);
|
||||
Assert.Equal(0, output[2]);
|
||||
Assert.Equal(0, output[3]);
|
||||
Assert.Equal(0, output[4]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_PopulationVsSample()
|
||||
{
|
||||
double[] source = [10, 20, 30];
|
||||
double[] outputPop = new double[3];
|
||||
double[] outputSamp = new double[3];
|
||||
|
||||
Variance.Batch(source, outputPop, 3, isPopulation: true);
|
||||
Variance.Batch(source, outputSamp, 3, isPopulation: false);
|
||||
|
||||
// Population variance should be smaller than sample variance
|
||||
Assert.True(outputPop[2] < outputSamp[2]);
|
||||
Assert.Equal(66.666666, outputPop[2], precision: 5); // 200/3
|
||||
Assert.Equal(100, outputSamp[2], precision: 6); // 200/2
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Fact]
|
||||
public void Resync_PreventsDrift_Extended()
|
||||
{
|
||||
// Test that resync works by running many updates
|
||||
var variance = new Variance(5);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.1, seed: 42);
|
||||
|
||||
// Run enough updates to trigger multiple resyncs
|
||||
for (int i = 0; i < 2500; i++)
|
||||
{
|
||||
variance.Update(new TValue(DateTime.UtcNow, gbm.Next().Close));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(variance.Last.Value));
|
||||
Assert.True(variance.Last.Value >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithNegativeValues()
|
||||
{
|
||||
var variance = new Variance(3);
|
||||
|
||||
variance.Update(new TValue(DateTime.UtcNow, -10));
|
||||
variance.Update(new TValue(DateTime.UtcNow, -5));
|
||||
variance.Update(new TValue(DateTime.UtcNow, 0));
|
||||
|
||||
Assert.Equal(25, variance.Last.Value, precision: 6); // Var([-10,-5,0]) = 25
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MixedPositiveNegative()
|
||||
{
|
||||
var variance = new Variance(4);
|
||||
|
||||
variance.Update(new TValue(DateTime.UtcNow, -2));
|
||||
variance.Update(new TValue(DateTime.UtcNow, -1));
|
||||
variance.Update(new TValue(DateTime.UtcNow, 1));
|
||||
variance.Update(new TValue(DateTime.UtcNow, 2));
|
||||
|
||||
Assert.Equal(10.0/3.0, variance.Last.Value, precision: 6); // Var([-2,-1,1,2]) = 10/3 ≈ 3.333
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_SimdFallback_WithNaN()
|
||||
{
|
||||
// Dataset with NaN should fall back to scalar path
|
||||
int count = 300;
|
||||
double[] source = new double[count];
|
||||
double[] output = new double[count];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
source[i] = i * 0.1;
|
||||
}
|
||||
source[150] = double.NaN; // Insert NaN
|
||||
|
||||
Variance.Batch(source, output, 10);
|
||||
|
||||
// Should complete without error
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]) || double.IsNaN(output[i]));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithPopulationFlag()
|
||||
{
|
||||
var popVariance = new Variance(5, isPopulation: true);
|
||||
var sampVariance = new Variance(5, isPopulation: false);
|
||||
|
||||
// Both should be valid
|
||||
Assert.NotNull(popVariance);
|
||||
Assert.NotNull(sampVariance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_Property_ContainsPeriod()
|
||||
{
|
||||
var variance = new Variance(10);
|
||||
Assert.Contains("10", variance.Name, StringComparison.Ordinal);
|
||||
Assert.Contains("Variance", variance.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_Property()
|
||||
{
|
||||
var variance = new Variance(7);
|
||||
Assert.Equal(7, variance.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_AfterReset_Works()
|
||||
{
|
||||
var variance = new Variance(3);
|
||||
|
||||
// Fill buffer
|
||||
variance.Update(new TValue(DateTime.UtcNow, 1));
|
||||
variance.Update(new TValue(DateTime.UtcNow, 2));
|
||||
variance.Update(new TValue(DateTime.UtcNow, 3));
|
||||
double valueBefore = variance.Last.Value;
|
||||
|
||||
variance.Reset();
|
||||
|
||||
// Update after reset
|
||||
variance.Update(new TValue(DateTime.UtcNow, 10));
|
||||
variance.Update(new TValue(DateTime.UtcNow, 20));
|
||||
variance.Update(new TValue(DateTime.UtcNow, 30));
|
||||
double valueAfter = variance.Last.Value;
|
||||
|
||||
Assert.NotEqual(valueBefore, valueAfter);
|
||||
Assert.Equal(100.0, valueAfter, precision: 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_ZeroLengthSpans()
|
||||
{
|
||||
double[] emptySource = [];
|
||||
double[] emptyOutput = [];
|
||||
|
||||
// Should not throw
|
||||
Variance.Batch(emptySource, emptyOutput, 2);
|
||||
|
||||
Assert.Empty(emptySource);
|
||||
Assert.Empty(emptyOutput);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_MinimalValidData()
|
||||
{
|
||||
double[] source = [10, 20];
|
||||
double[] output = new double[2];
|
||||
|
||||
Variance.Batch(source, output, 2);
|
||||
|
||||
Assert.Equal(0, output[0]); // N=1
|
||||
Assert.Equal(50, output[1]); // Var([10,20]) = 50
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
using QuanTAlib.Tests;
|
||||
using Skender.Stock.Indicators;
|
||||
using TALib;
|
||||
using Tulip;
|
||||
using OoplesFinance.StockIndicators;
|
||||
using OoplesFinance.StockIndicators.Models;
|
||||
using OoplesFinance.StockIndicators.Enums;
|
||||
using MathNet.Numerics.Statistics;
|
||||
|
||||
namespace QuanTAlib.Validation;
|
||||
|
||||
@@ -5,21 +5,229 @@ namespace QuanTAlib.Quantower.Tests;
|
||||
public class UsfIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Indicator_InitializesCorrectly()
|
||||
public void UsfIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new UsfIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal("USF 20:Close", indicator.ShortName);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("USF - Ultimate Smoother Filter", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indicator_ProcessesData()
|
||||
public void UsfIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new UsfIndicator();
|
||||
|
||||
// Simulate Init
|
||||
indicator.GetType().GetMethod("OnInit", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.Invoke(indicator, null);
|
||||
Assert.Equal(0, UsfIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UsfIndicator_ShortName_IncludesPeriodAndSource()
|
||||
{
|
||||
var indicator = new UsfIndicator { Period = 14 };
|
||||
|
||||
Assert.True(indicator.ShortName.Contains("USF", StringComparison.Ordinal));
|
||||
Assert.True(indicator.ShortName.Contains("14", StringComparison.Ordinal));
|
||||
Assert.True(indicator.ShortName.Contains("Close", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UsfIndicator_Initialize_CreatesInternalUsf()
|
||||
{
|
||||
var indicator = new UsfIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UsfIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new UsfIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have a value
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UsfIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new UsfIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UsfIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new UsfIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Should not throw an exception
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
// Assert that the indicator still exists (method completed without exception)
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UsfIndicator_MultipleUpdates_ProducesCorrectSequence()
|
||||
{
|
||||
var indicator = new UsfIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 102, 105, 103, 107, 110 };
|
||||
|
||||
foreach (var close in closes)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
// All values should be finite
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UsfIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new UsfIndicator { Period = 5, Source = source };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
|
||||
$"Source {source} should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UsfIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new UsfIndicator { Period = 10 };
|
||||
|
||||
Assert.Equal(10, indicator.Period);
|
||||
|
||||
indicator.Period = 20;
|
||||
Assert.Equal(20, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UsfIndicator_Source_CanBeChanged()
|
||||
{
|
||||
var indicator = new UsfIndicator { Source = SourceType.Close };
|
||||
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
|
||||
indicator.Source = SourceType.Open;
|
||||
Assert.Equal(SourceType.Open, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UsfIndicator_ShowColdValues_CanBeChanged()
|
||||
{
|
||||
var indicator = new UsfIndicator { ShowColdValues = true };
|
||||
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UsfIndicator_ShortName_UpdatesWhenPeriodChanges()
|
||||
{
|
||||
var indicator = new UsfIndicator { Period = 10 };
|
||||
string initialName = indicator.ShortName;
|
||||
|
||||
Assert.True(initialName.Contains("10", StringComparison.Ordinal));
|
||||
|
||||
indicator.Period = 20;
|
||||
string updatedName = indicator.ShortName;
|
||||
|
||||
Assert.True(updatedName.Contains("20", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UsfIndicator_ShortName_UpdatesWhenSourceChanges()
|
||||
{
|
||||
var indicator = new UsfIndicator { Source = SourceType.Close };
|
||||
string initialName = indicator.ShortName;
|
||||
|
||||
Assert.True(initialName.Contains("Close", StringComparison.Ordinal));
|
||||
|
||||
indicator.Source = SourceType.Open;
|
||||
string updatedName = indicator.ShortName;
|
||||
|
||||
Assert.True(updatedName.Contains("Open", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UsfIndicator_ProcessUpdate_IgnoresNonBarUpdates()
|
||||
{
|
||||
var indicator = new UsfIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process historical bar first
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Process other update reasons - should not throw
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
// Assert that the indicator still exists (method completed without exception)
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UsfIndicator_LineSeries_HasCorrectProperties()
|
||||
{
|
||||
var indicator = new UsfIndicator { Period = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var lineSeries = indicator.LinesSeries[0];
|
||||
|
||||
Assert.True(lineSeries.Name.Contains("USF 20", StringComparison.Ordinal)); // LineSeries name is set in constructor with default period
|
||||
Assert.Equal(2, lineSeries.Width);
|
||||
Assert.Equal(LineStyle.Solid, lineSeries.Style);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,20 +18,18 @@ public sealed class UsfIndicator : Indicator, IWatchlistIndicator
|
||||
|
||||
private Usf? _ma;
|
||||
private readonly LineSeries? _series;
|
||||
private string? _sourceName;
|
||||
private Func<IHistoryItem, double>? _priceSelector;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"USF {Period}:{_sourceName}";
|
||||
public override string ShortName => $"USF {Period}:{Source}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/trends/usf/Usf.Quantower.cs";
|
||||
|
||||
public UsfIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
_sourceName = Source.ToString();
|
||||
Name = "USF - Ultimate Smoother Filter";
|
||||
Description = "Ehlers Ultimate Smoother Filter";
|
||||
_series = new(name: $"USF {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
|
||||
@@ -42,7 +40,6 @@ public sealed class UsfIndicator : Indicator, IWatchlistIndicator
|
||||
protected override void OnInit()
|
||||
{
|
||||
_ma = new Usf(Period);
|
||||
_sourceName = Source.ToString();
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
@@ -58,3 +58,4 @@ exclude:
|
||||
- name: EnforceIfStatementBraces
|
||||
- name: CheckNamespace
|
||||
- name: LoopCanBeConvertedToQuery
|
||||
- name: UnusedAutoPropertyAccessor.Global
|
||||
|
||||
Reference in New Issue
Block a user