mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-06 21:17:44 +00:00
060649192f
- Remove 'C# Implementation Considerations' sections from 34 indicator .md files - Delete 29 temp PowerShell scripts (_fix_mojibake.ps1, _hex_scan.ps1, etc.) - Move test files into tests/ subdirectories for consistent project structure - Add trader-focused bullet points to indicator documentation
581 lines
17 KiB
C#
581 lines
17 KiB
C#
namespace QuanTAlib.Tests;
|
|
|
|
public class DsmaTests
|
|
{
|
|
[Fact]
|
|
public void Dsma_ConstructorValidation_ThrowsOnInvalidPeriod()
|
|
{
|
|
// Arrange & Act & Assert
|
|
var ex1 = Assert.Throws<ArgumentOutOfRangeException>(() => new Dsma(1));
|
|
Assert.Equal("period", ex1.ParamName);
|
|
|
|
var ex2 = Assert.Throws<ArgumentOutOfRangeException>(() => new Dsma(0));
|
|
Assert.Equal("period", ex2.ParamName);
|
|
|
|
var ex3 = Assert.Throws<ArgumentOutOfRangeException>(() => new Dsma(-5));
|
|
Assert.Equal("period", ex3.ParamName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Dsma_ConstructorValidation_ThrowsOnInvalidScaleFactor()
|
|
{
|
|
// Arrange & Act & Assert
|
|
var ex1 = Assert.Throws<ArgumentOutOfRangeException>(() => new Dsma(10, 0.005));
|
|
Assert.Equal("scaleFactor", ex1.ParamName);
|
|
|
|
var ex2 = Assert.Throws<ArgumentOutOfRangeException>(() => new Dsma(10, 0.95));
|
|
Assert.Equal("scaleFactor", ex2.ParamName);
|
|
|
|
var ex3 = Assert.Throws<ArgumentOutOfRangeException>(() => new Dsma(10, -0.1));
|
|
Assert.Equal("scaleFactor", ex3.ParamName);
|
|
|
|
var ex4 = Assert.Throws<ArgumentOutOfRangeException>(() => new Dsma(10, 1.5));
|
|
Assert.Equal("scaleFactor", ex4.ParamName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Dsma_ConstructorValidation_AcceptsValidParameters()
|
|
{
|
|
// Arrange & Act
|
|
var dsma1 = new Dsma(2, 0.01);
|
|
var dsma2 = new Dsma(100, 0.9);
|
|
var dsma3 = new Dsma(25, 0.5);
|
|
|
|
// Assert
|
|
Assert.NotNull(dsma1);
|
|
Assert.NotNull(dsma2);
|
|
Assert.NotNull(dsma3);
|
|
Assert.Equal("Dsma(2,0.01)", dsma1.Name);
|
|
Assert.Equal("Dsma(100,0.90)", dsma2.Name);
|
|
Assert.Equal("Dsma(25,0.50)", dsma3.Name);
|
|
}
|
|
|
|
[Fact]
|
|
public void Dsma_BasicCalculation_ReturnsExpectedValues()
|
|
{
|
|
// Arrange
|
|
var dsma = new Dsma(period: 5, scaleFactor: 0.5);
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
|
|
|
// Act
|
|
TValue result = default;
|
|
for (int i = 0; i < 20; i++)
|
|
{
|
|
var bar = gbm.Next(isNew: true);
|
|
result = dsma.Update(new TValue(bar.Time, bar.Close));
|
|
}
|
|
|
|
// Assert
|
|
Assert.NotEqual(0.0, result.Value);
|
|
Assert.True(double.IsFinite(result.Value));
|
|
Assert.True(dsma.IsHot);
|
|
}
|
|
|
|
[Fact]
|
|
public void Dsma_Properties_AccessibleAndCorrect()
|
|
{
|
|
// Arrange
|
|
var dsma = new Dsma(period: 10, scaleFactor: 0.6);
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 100);
|
|
|
|
// Act
|
|
for (int i = 0; i < 15; i++)
|
|
{
|
|
var bar = gbm.Next(isNew: true);
|
|
dsma.Update(new TValue(bar.Time, bar.Close));
|
|
}
|
|
|
|
// Assert
|
|
Assert.NotEqual(default, dsma.Last);
|
|
Assert.True(dsma.IsHot);
|
|
Assert.Equal(10, dsma.WarmupPeriod);
|
|
Assert.Equal("Dsma(10,0.60)", dsma.Name);
|
|
}
|
|
|
|
[Fact]
|
|
public void Dsma_StateAndBarCorrection_IsNewTrue()
|
|
{
|
|
// Arrange
|
|
var dsma = new Dsma(period: 5, scaleFactor: 0.5);
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 50);
|
|
|
|
// Act - Add values with isNew=true
|
|
TValue last = default;
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
var bar = gbm.Next(isNew: true);
|
|
last = dsma.Update(new TValue(bar.Time, bar.Close), isNew: true);
|
|
}
|
|
|
|
// Assert
|
|
Assert.True(double.IsFinite(last.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Dsma_StateAndBarCorrection_IsNewFalse()
|
|
{
|
|
// Arrange
|
|
var dsma = new Dsma(period: 5, scaleFactor: 0.5);
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 60);
|
|
|
|
// Act - Add first 9 values normally
|
|
for (int i = 0; i < 9; i++)
|
|
{
|
|
var bar = gbm.Next(isNew: true);
|
|
dsma.Update(new TValue(bar.Time, bar.Close), isNew: true);
|
|
}
|
|
|
|
var beforeCorrection = dsma.Last;
|
|
|
|
// Update last bar multiple times
|
|
var lastBar = gbm.Next(isNew: true);
|
|
dsma.Update(new TValue(lastBar.Time, lastBar.Close), isNew: true);
|
|
var firstUpdate = dsma.Last;
|
|
|
|
dsma.Update(new TValue(lastBar.Time, lastBar.Close * 1.1), isNew: false);
|
|
var corrected = dsma.Last;
|
|
|
|
// Assert
|
|
Assert.NotEqual(beforeCorrection.Value, firstUpdate.Value);
|
|
Assert.NotEqual(firstUpdate.Value, corrected.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void Dsma_IterativeCorrection_RestoresState()
|
|
{
|
|
// Arrange
|
|
var dsma = new Dsma(period: 5, scaleFactor: 0.5);
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 70);
|
|
|
|
// Act - Process first 9 bars
|
|
for (int i = 0; i < 9; i++)
|
|
{
|
|
var bar = gbm.Next(isNew: true);
|
|
dsma.Update(new TValue(bar.Time, bar.Close), isNew: true);
|
|
}
|
|
|
|
// Process bar 10 with multiple corrections
|
|
var lastBar = gbm.Next(isNew: true);
|
|
var lastInput = new TValue(lastBar.Time, lastBar.Close);
|
|
dsma.Update(lastInput, isNew: true);
|
|
var original = dsma.Last.Value;
|
|
|
|
dsma.Update(new TValue(lastBar.Time, lastBar.Close * 1.2), isNew: false);
|
|
dsma.Update(new TValue(lastBar.Time, lastBar.Close * 0.8), isNew: false);
|
|
dsma.Update(lastInput, isNew: false); // Restore to original
|
|
|
|
var restored = dsma.Last.Value;
|
|
|
|
// Assert - Should be very close to original
|
|
Assert.Equal(original, restored, precision: 6);
|
|
}
|
|
|
|
[Fact]
|
|
public void Dsma_Reset_ClearsState()
|
|
{
|
|
// Arrange
|
|
var dsma = new Dsma(period: 5, scaleFactor: 0.5);
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 80);
|
|
|
|
// Act - Process data
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
var bar = gbm.Next(isNew: true);
|
|
dsma.Update(new TValue(bar.Time, bar.Close));
|
|
}
|
|
|
|
Assert.True(dsma.IsHot);
|
|
|
|
// Reset
|
|
dsma.Reset();
|
|
|
|
// Assert
|
|
Assert.False(dsma.IsHot);
|
|
Assert.Equal(default, dsma.Last);
|
|
}
|
|
|
|
[Fact]
|
|
public void Dsma_WarmupPeriod_IsHotTransition()
|
|
{
|
|
// Arrange
|
|
var period = 10;
|
|
var dsma = new Dsma(period, scaleFactor: 0.5);
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 90);
|
|
|
|
// Act & Assert
|
|
for (int i = 0; i < period - 1; i++)
|
|
{
|
|
var bar = gbm.Next(isNew: true);
|
|
dsma.Update(new TValue(bar.Time, bar.Close));
|
|
Assert.False(dsma.IsHot, $"Should not be hot at bar {i + 1}");
|
|
}
|
|
|
|
var lastBar = gbm.Next(isNew: true);
|
|
dsma.Update(new TValue(lastBar.Time, lastBar.Close));
|
|
Assert.True(dsma.IsHot, $"Should be hot at bar {period}");
|
|
}
|
|
|
|
[Fact]
|
|
public void Dsma_RobustnessNaN_UsesLastValidValue()
|
|
{
|
|
// Arrange
|
|
var dsma = new Dsma(period: 5, scaleFactor: 0.5);
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 100);
|
|
|
|
// Act - Process normal data
|
|
TBar lastBar;
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
lastBar = gbm.Next(isNew: true);
|
|
dsma.Update(new TValue(lastBar.Time, lastBar.Close));
|
|
}
|
|
|
|
// Get the last bar again after loop
|
|
lastBar = gbm.Next(isNew: false);
|
|
|
|
// Inject NaN
|
|
var nanResult = dsma.Update(new TValue(lastBar.Time, double.NaN));
|
|
|
|
// Assert - Should use last valid value (not propagate NaN)
|
|
Assert.True(double.IsFinite(nanResult.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Dsma_RobustnessInfinity_UsesLastValidValue()
|
|
{
|
|
// Arrange
|
|
var dsma = new Dsma(period: 5, scaleFactor: 0.5);
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 110);
|
|
|
|
// Act - Process normal data
|
|
TBar lastBar;
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
lastBar = gbm.Next(isNew: true);
|
|
dsma.Update(new TValue(lastBar.Time, lastBar.Close));
|
|
}
|
|
|
|
// Get the last bar again after loop
|
|
lastBar = gbm.Next(isNew: false);
|
|
|
|
// Inject Infinity
|
|
var infResult = dsma.Update(new TValue(lastBar.Time, double.PositiveInfinity));
|
|
var negInfResult = dsma.Update(new TValue(lastBar.Time, double.NegativeInfinity));
|
|
|
|
// Assert - Should use last valid value
|
|
Assert.True(double.IsFinite(infResult.Value));
|
|
Assert.True(double.IsFinite(negInfResult.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Dsma_RobustnessBatchNaN_Handles()
|
|
{
|
|
// Arrange
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 120);
|
|
var series = new TSeries();
|
|
for (int i = 0; i < 20; i++)
|
|
{
|
|
var bar = gbm.Next(isNew: true);
|
|
double value;
|
|
if (i == 10)
|
|
{
|
|
value = double.NaN;
|
|
}
|
|
else if (i == 15)
|
|
{
|
|
value = double.PositiveInfinity;
|
|
}
|
|
else
|
|
{
|
|
value = bar.Close;
|
|
}
|
|
series.Add(bar.Time, value);
|
|
}
|
|
|
|
// Act
|
|
var result = Dsma.Batch(series, period: 5, scaleFactor: 0.5);
|
|
|
|
// Assert
|
|
Assert.Equal(20, result.Count);
|
|
Assert.All(result.Values.ToArray(), val => Assert.True(double.IsFinite(val)));
|
|
}
|
|
|
|
[Fact]
|
|
public void Dsma_ConsistencyBatchVsStreaming()
|
|
{
|
|
// Arrange
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 130);
|
|
var series = new TSeries();
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
var bar = gbm.Next(isNew: true);
|
|
series.Add(bar.Time, bar.Close);
|
|
}
|
|
var period = 10;
|
|
var scale = 0.6;
|
|
|
|
// Act - Batch
|
|
var batchResult = Dsma.Batch(series, period, scale);
|
|
|
|
// Act - Streaming
|
|
var dsma = new Dsma(period, scale);
|
|
var streamResult = new List<double>();
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
streamResult.Add(dsma.Update(series[i]).Value);
|
|
}
|
|
|
|
// Assert - All values should match
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
Assert.Equal(batchResult.Values[i], streamResult[i], precision: 10);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Dsma_ConsistencyBatchVsSpan()
|
|
{
|
|
// Arrange
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 140);
|
|
var series = new TSeries();
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
var bar = gbm.Next(isNew: true);
|
|
series.Add(bar.Time, bar.Close);
|
|
}
|
|
var values = series.Values.ToArray();
|
|
var period = 10;
|
|
var scale = 0.6;
|
|
|
|
// Act - Batch (TSeries)
|
|
var batchResult = Dsma.Batch(series, period, scale);
|
|
|
|
// Act - Span
|
|
var spanOutput = new double[values.Length];
|
|
Dsma.Batch(values, spanOutput, period, scale);
|
|
|
|
// Assert
|
|
for (int i = 0; i < values.Length; i++)
|
|
{
|
|
Assert.Equal(batchResult.Values[i], spanOutput[i], precision: 10);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Dsma_ConsistencyStreamingVsSpan()
|
|
{
|
|
// Arrange
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 150);
|
|
var series = new TSeries();
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
var bar = gbm.Next(isNew: true);
|
|
series.Add(bar.Time, bar.Close);
|
|
}
|
|
var values = series.Values.ToArray();
|
|
var period = 10;
|
|
var scale = 0.6;
|
|
|
|
// Act - Streaming
|
|
var dsma = new Dsma(period, scale);
|
|
var streamResult = new List<double>();
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
streamResult.Add(dsma.Update(series[i]).Value);
|
|
}
|
|
|
|
// Act - Span
|
|
var spanOutput = new double[values.Length];
|
|
Dsma.Batch(values, spanOutput, period, scale);
|
|
|
|
// Assert
|
|
for (int i = 0; i < values.Length; i++)
|
|
{
|
|
Assert.Equal(streamResult[i], spanOutput[i], precision: 10);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Dsma_ConsistencyEventing()
|
|
{
|
|
// Arrange
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 160);
|
|
var source = new TSeries();
|
|
var period = 10;
|
|
var scale = 0.6;
|
|
|
|
var eventResults = new List<TValue>();
|
|
var dsma = new Dsma(source, period, scale);
|
|
dsma.Pub += (sender, in args) => eventResults.Add(args.Value);
|
|
|
|
// Act
|
|
var series = new TSeries();
|
|
for (int i = 0; i < 30; i++)
|
|
{
|
|
var bar = gbm.Next(isNew: true);
|
|
var tval = new TValue(bar.Time, bar.Close);
|
|
series.Add(tval);
|
|
source.Add(tval);
|
|
}
|
|
|
|
// Assert
|
|
Assert.Equal(30, eventResults.Count);
|
|
|
|
// Compare with direct calculation
|
|
var directDsma = new Dsma(period, scale);
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
var expected = directDsma.Update(series[i]).Value;
|
|
Assert.Equal(expected, eventResults[i].Value, precision: 10);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Dsma_SpanValidation_ThrowsOnShortOutput()
|
|
{
|
|
// Arrange
|
|
var source = new double[100];
|
|
var shortOutput = new double[50];
|
|
|
|
// Act & Assert
|
|
var ex = Assert.Throws<ArgumentException>(() =>
|
|
Dsma.Batch(source, shortOutput, period: 10));
|
|
Assert.Equal("output", ex.ParamName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Dsma_SpanValidation_AcceptsEqualLength()
|
|
{
|
|
// Arrange
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 170);
|
|
var values = new double[50];
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
var bar = gbm.Next(isNew: true);
|
|
values[i] = bar.Close;
|
|
}
|
|
var output = new double[50];
|
|
|
|
// Act
|
|
Dsma.Batch(values, output, period: 10, scaleFactor: 0.5);
|
|
|
|
// Assert
|
|
Assert.All(output, val => Assert.True(double.IsFinite(val)));
|
|
}
|
|
|
|
[Fact]
|
|
public void Dsma_SpanValidation_AcceptsLongerOutput()
|
|
{
|
|
// Arrange
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 180);
|
|
var values = new double[50];
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
var bar = gbm.Next(isNew: true);
|
|
values[i] = bar.Close;
|
|
}
|
|
var output = new double[100];
|
|
|
|
// Act
|
|
Dsma.Batch(values, output, period: 10, scaleFactor: 0.5);
|
|
|
|
// Assert
|
|
Assert.All(output.Take(50), val => Assert.True(double.IsFinite(val)));
|
|
}
|
|
|
|
[Fact]
|
|
public void Dsma_SpanHandlesNaN()
|
|
{
|
|
// Arrange
|
|
var values = new double[20];
|
|
Array.Fill(values, 100.0);
|
|
values[10] = double.NaN;
|
|
var output = new double[20];
|
|
|
|
// Act
|
|
Dsma.Batch(values, output, period: 5, scaleFactor: 0.5);
|
|
|
|
// Assert
|
|
Assert.All(output, val => Assert.True(double.IsFinite(val)));
|
|
}
|
|
|
|
[Fact]
|
|
public void Dsma_Chainability_WorksWithPub()
|
|
{
|
|
// Arrange
|
|
var source = new TSeries();
|
|
var dsma = new Dsma(source, period: 5, scaleFactor: 0.5);
|
|
var receivedEvents = 0;
|
|
|
|
dsma.Pub += (sender, in args) => receivedEvents++;
|
|
|
|
// Act
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 190);
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
var bar = gbm.Next(isNew: true);
|
|
source.Add(bar.Time, bar.Close);
|
|
}
|
|
|
|
// Assert
|
|
Assert.Equal(10, receivedEvents);
|
|
}
|
|
|
|
[Fact]
|
|
public void Dsma_DifferentScaleFactors_ProduceDifferentResults()
|
|
{
|
|
// Arrange
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 200);
|
|
var dsmaLow = new Dsma(period: 10, scaleFactor: 0.1);
|
|
var dsmaHigh = new Dsma(period: 10, scaleFactor: 0.8);
|
|
|
|
// Act
|
|
TValue resultLow = default, resultHigh = default;
|
|
for (int i = 0; i < 30; i++)
|
|
{
|
|
var bar = gbm.Next(isNew: true);
|
|
var tval = new TValue(bar.Time, bar.Close);
|
|
resultLow = dsmaLow.Update(tval);
|
|
resultHigh = dsmaHigh.Update(tval);
|
|
}
|
|
|
|
// Assert - Different scale factors should produce different results
|
|
Assert.NotEqual(resultLow.Value, resultHigh.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void Dsma_FirstBarInitialization()
|
|
{
|
|
// Arrange
|
|
var dsma = new Dsma(period: 5, scaleFactor: 0.5);
|
|
|
|
// Act
|
|
var result = dsma.Update(new TValue(DateTime.UtcNow, 100.0));
|
|
|
|
// Assert - First bar should equal input
|
|
Assert.Equal(100.0, result.Value, precision: 10);
|
|
Assert.False(dsma.IsHot);
|
|
}
|
|
|
|
[Fact]
|
|
public void Dsma_Prime_PopulatesIndicator()
|
|
{
|
|
// Arrange
|
|
var dsma = new Dsma(period: 10, scaleFactor: 0.5);
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 210);
|
|
var values = new double[20];
|
|
for (int i = 0; i < 20; i++)
|
|
{
|
|
var bar = gbm.Next(isNew: true);
|
|
values[i] = bar.Close;
|
|
}
|
|
|
|
// Act
|
|
dsma.Prime(values);
|
|
|
|
// Assert
|
|
Assert.True(dsma.IsHot);
|
|
Assert.NotEqual(default, dsma.Last);
|
|
}
|
|
}
|