docs: remove C# Implementation Considerations sections, clean up temp scripts, reorganize test files

- 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
This commit is contained in:
Miha Kralj
2026-03-12 12:34:16 -07:00
parent 8937b0c0fa
commit 060649192f
1149 changed files with 1780 additions and 3316 deletions
@@ -0,0 +1,215 @@
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
public class DsmaIndicatorTests
{
[Fact]
public void DsmaIndicator_Constructor_SetsDefaults()
{
var indicator = new DsmaIndicator();
Assert.Equal(20, indicator.Period);
Assert.Equal(0.5, indicator.ScaleFactor);
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Equal("DSMA - Deviation-Scaled Moving Average", indicator.Name);
Assert.False(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void DsmaIndicator_MinHistoryDepths_ReturnsZero()
{
var indicator = new DsmaIndicator { Period = 20 };
Assert.Equal(0, DsmaIndicator.MinHistoryDepths);
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void DsmaIndicator_ShortName_IncludesParameters()
{
var indicator = new DsmaIndicator { Period = 15, ScaleFactor = 0.6 };
Assert.Contains("DSMA", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("15", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("0.60", indicator.ShortName, StringComparison.Ordinal);
}
[Fact]
public void DsmaIndicator_SourceCodeLink_IsValid()
{
var indicator = new DsmaIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("Dsma.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
[Fact]
public void DsmaIndicator_Initialize_CreatesInternalDsma()
{
var indicator = new DsmaIndicator { Period = 10 };
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void DsmaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new DsmaIndicator { 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 DsmaIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new DsmaIndicator { 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 DsmaIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new DsmaIndicator { 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 DsmaIndicator_MultipleUpdates_ProducesCorrectSequence()
{
var indicator = new DsmaIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
double[] closes = { 100, 102, 104, 103, 105 };
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 DsmaIndicator_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 DsmaIndicator { 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 DsmaIndicator_Parameters_CanBeChanged()
{
var indicator = new DsmaIndicator { Period = 10, ScaleFactor = 0.3 };
Assert.Equal(10, indicator.Period);
Assert.Equal(0.3, indicator.ScaleFactor);
indicator.Period = 20;
indicator.ScaleFactor = 0.7;
Assert.Equal(20, indicator.Period);
Assert.Equal(0.7, indicator.ScaleFactor);
Assert.Equal(0, DsmaIndicator.MinHistoryDepths);
}
[Fact]
public void DsmaIndicator_ScaleFactorBounds_Work()
{
var indicator = new DsmaIndicator();
// Test minimum bound
indicator.ScaleFactor = 0.01;
Assert.Equal(0.01, indicator.ScaleFactor);
// Test maximum bound
indicator.ScaleFactor = 0.9;
Assert.Equal(0.9, indicator.ScaleFactor);
// Test mid-range
indicator.ScaleFactor = 0.5;
Assert.Equal(0.5, indicator.ScaleFactor);
}
[Fact]
public void DsmaIndicator_ProcessUpdate_BarCorrection_HandlesIsNew()
{
var indicator = new DsmaIndicator { Period = 5, ScaleFactor = 0.5 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 100);
indicator.HistoricalData.AddBar(now.AddMinutes(1), 100, 110, 98, 105);
// Process first bar
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// Process second bar as new
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
double afterNewBar = indicator.LinesSeries[0].GetValue(0);
// Update same bar (bar correction)
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
double afterTick = indicator.LinesSeries[0].GetValue(0);
// Both should be finite
Assert.True(double.IsFinite(afterNewBar));
Assert.True(double.IsFinite(afterTick));
}
}
+580
View File
@@ -0,0 +1,580 @@
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);
}
}
@@ -0,0 +1,358 @@
using OoplesFinance.StockIndicators;
using OoplesFinance.StockIndicators.Models;
namespace QuanTAlib.Tests;
public class DsmaValidationTests
{
[Fact]
public void Dsma_FollowsPriceTrend()
{
// DSMA should generally follow price trends due to Super Smoother filter
// In an uptrend, DSMA should eventually trend upward
var dsma = new Dsma(period: 10, scaleFactor: 0.5);
double previousDsma = 0;
int increasingCount = 0;
// Uptrend: steadily increasing prices
for (int i = 0; i < 100; i++)
{
var result = dsma.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i));
if (i > 20 && result.Value > previousDsma) // Allow warmup
{
increasingCount++;
}
previousDsma = result.Value;
}
// DSMA should be increasing in most bars during uptrend (allow some lag)
Assert.True(increasingCount > 60, $"DSMA should follow uptrend, increased in {increasingCount} out of 80 bars");
}
[Fact]
public void Dsma_ResponsivenessToVolatility()
{
// DSMA adapts to volatility via RMS-based scaling
// Higher volatility should produce more responsive behavior
var gbmLowVol = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.05, seed: 42);
var gbmHighVol = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.5, seed: 42);
var dsmaLowVol = new Dsma(period: 20, scaleFactor: 0.5);
var dsmaHighVol = new Dsma(period: 20, scaleFactor: 0.5);
double lowVolDeviation = 0;
double highVolDeviation = 0;
for (int i = 0; i < 100; i++)
{
var barLow = gbmLowVol.Next(isNew: true);
var barHigh = gbmHighVol.Next(isNew: true);
var resultLow = dsmaLowVol.Update(new TValue(barLow.Time, barLow.Close));
var resultHigh = dsmaHighVol.Update(new TValue(barHigh.Time, barHigh.Close));
if (i > 30) // After warmup
{
lowVolDeviation += Math.Abs(barLow.Close - resultLow.Value);
highVolDeviation += Math.Abs(barHigh.Close - resultHigh.Value);
}
}
// In higher volatility, absolute deviation should generally be larger
Assert.True(highVolDeviation > lowVolDeviation * 2,
$"High volatility deviation {highVolDeviation:F2} should be significantly larger than low volatility {lowVolDeviation:F2}");
}
[Fact]
public void Dsma_ScaleFactorEffect()
{
// Higher scaleFactor should make DSMA more responsive to price changes
// Lower scaleFactor should make it smoother
var gbm = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.3, seed: 123);
var dsmaLowScale = new Dsma(period: 20, scaleFactor: 0.1);
var dsmaHighScale = new Dsma(period: 20, scaleFactor: 0.8);
double lowScaleLag = 0;
double highScaleLag = 0;
int count = 0;
for (int i = 0; i < 200; i++)
{
var bar = gbm.Next(isNew: true);
var tval = new TValue(bar.Time, bar.Close);
var resultLow = dsmaLowScale.Update(tval);
var resultHigh = dsmaHighScale.Update(tval);
if (i > 30) // After warmup
{
lowScaleLag += Math.Abs(bar.Close - resultLow.Value);
highScaleLag += Math.Abs(bar.Close - resultHigh.Value);
count++;
}
}
double avgLowLag = lowScaleLag / count;
double avgHighLag = highScaleLag / count;
// Lower scale factor should have higher average lag (smoother, less responsive)
Assert.True(avgLowLag > avgHighLag,
$"Low scale lag {avgLowLag:F4} should be greater than high scale lag {avgHighLag:F4}");
}
[Fact]
public void Dsma_SmoothnessBehavior()
{
// DSMA should be smoother than raw price (lower variance)
// This validates the Super Smoother filter component
var gbm = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.2, seed: 456);
var dsma = new Dsma(period: 15, scaleFactor: 0.5);
var priceChanges = new List<double>();
var dsmaChanges = new List<double>();
double prevPrice = 100.0;
double prevDsma = 100.0;
for (int i = 0; i < 200; i++)
{
var bar = gbm.Next(isNew: true);
var result = dsma.Update(new TValue(bar.Time, bar.Close));
if (i > 30) // After warmup
{
priceChanges.Add(Math.Abs(bar.Close - prevPrice));
dsmaChanges.Add(Math.Abs(result.Value - prevDsma));
}
prevPrice = bar.Close;
prevDsma = result.Value;
}
double priceVariance = priceChanges.Average();
double dsmaVariance = dsmaChanges.Average();
// DSMA should have lower variance than raw price
Assert.True(dsmaVariance < priceVariance,
$"DSMA variance {dsmaVariance:F4} should be less than price variance {priceVariance:F4}");
}
[Fact]
public void Dsma_WithinBounds()
{
// DSMA should stay within reasonable bounds of recent prices
// It's an adaptive moving average, shouldn't overshoot wildly
var dsma = new Dsma(period: 10, scaleFactor: 0.5);
var gbm = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.3, seed: 789);
var recentPrices = new List<double>();
const int windowSize = 20;
for (int i = 0; i < 500; i++)
{
var bar = gbm.Next(isNew: true);
var result = dsma.Update(new TValue(bar.Time, bar.Close));
recentPrices.Add(bar.Close);
if (recentPrices.Count > windowSize)
{
recentPrices.RemoveAt(0);
}
if (i > 30 && recentPrices.Count == windowSize)
{
double minPrice = recentPrices.Min();
double maxPrice = recentPrices.Max();
double margin = (maxPrice - minPrice) * 0.3; // 30% margin for adaptive behavior
Assert.True(result.Value >= minPrice - margin && result.Value <= maxPrice + margin,
$"At index {i}: DSMA {result.Value:F2} outside bounds [{minPrice - margin:F2}, {maxPrice + margin:F2}]");
}
}
}
[Fact]
public void Dsma_ConsistentWarmup()
{
// DSMA should consistently reach IsHot state at expected period
var dsma = new Dsma(period: 15, scaleFactor: 0.5);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 321);
for (int i = 0; i < 14; 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 period boundary");
}
[Fact]
public void Dsma_ConvergenceAfterReset()
{
// After reset, DSMA should converge to similar values when fed same data
var gbm = new GBM(startPrice: 100.0, mu: 0.01, sigma: 0.2, seed: 654);
var series = new TSeries();
for (int i = 0; i < 100; i++)
{
var bar = gbm.Next(isNew: true);
series.Add(bar.Time, bar.Close);
}
// First run
var dsma1 = new Dsma(period: 10, scaleFactor: 0.5);
var result1 = dsma1.Update(series);
// Reset and second run
var dsma2 = new Dsma(period: 10, scaleFactor: 0.5);
var result2 = dsma2.Update(series);
// Compare last 50 values
for (int i = 50; i < 100; i++)
{
Assert.Equal(result1.Values[i], result2.Values[i], precision: 10);
}
}
[Fact]
public void Dsma_PeriodEffect()
{
// Longer period should produce smoother results with more lag
var gbm = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.25, seed: 987);
var dsmaShort = new Dsma(period: 5, scaleFactor: 0.5);
var dsmaLong = new Dsma(period: 30, scaleFactor: 0.5);
double shortLag = 0;
double longLag = 0;
int count = 0;
for (int i = 0; i < 200; i++)
{
var bar = gbm.Next(isNew: true);
var tval = new TValue(bar.Time, bar.Close);
var resultShort = dsmaShort.Update(tval);
var resultLong = dsmaLong.Update(tval);
if (i > 40) // After both warmed up
{
shortLag += Math.Abs(bar.Close - resultShort.Value);
longLag += Math.Abs(bar.Close - resultLong.Value);
count++;
}
}
double avgShortLag = shortLag / count;
double avgLongLag = longLag / count;
// Longer period should have higher average lag (more smoothing)
Assert.True(avgLongLag > avgShortLag,
$"Long period lag {avgLongLag:F4} should be greater than short period lag {avgShortLag:F4}");
}
[Fact]
public void Dsma_MathematicalConsistency()
{
// Verify that DSMA maintains mathematical consistency:
// - Output is always finite
// - Sequential updates produce deterministic results
// - Values remain reasonable
var gbm = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.3, seed: 111);
var dsma = new Dsma(period: 12, scaleFactor: 0.5);
for (int i = 0; i < 300; i++)
{
var bar = gbm.Next(isNew: true);
var result = dsma.Update(new TValue(bar.Time, bar.Close));
// Always finite
Assert.True(double.IsFinite(result.Value), $"DSMA should be finite at index {i}");
// DSMA should remain positive for positive prices
Assert.True(result.Value > 0, $"DSMA should be positive at index {i}");
// DSMA should stay within reasonable range of price (allow wide margin for adaptive behavior)
if (i > 20)
{
Assert.True(result.Value > bar.Close * 0.5 && result.Value < bar.Close * 1.5,
$"At index {i}: DSMA {result.Value:F2} outside reasonable range of price {bar.Close:F2}");
}
}
}
[Fact]
public void Dsma_SuperSmootherComponent()
{
// Validate that the Super Smoother (Butterworth) filter component
// provides noise reduction while maintaining trend following
var gbm = new GBM(startPrice: 100.0, mu: 0.03, sigma: 0.3, seed: 222);
var dsma = new Dsma(period: 20, scaleFactor: 0.5);
var prices = new List<double>();
var dsmaValues = new List<double>();
for (int i = 0; i < 200; i++)
{
var bar = gbm.Next(isNew: true);
var result = dsma.Update(new TValue(bar.Time, bar.Close));
if (i > 30)
{
prices.Add(bar.Close);
dsmaValues.Add(result.Value);
}
}
// Calculate directional consistency
int priceUpCount = 0;
int dsmaUpCount = 0;
for (int i = 1; i < prices.Count; i++)
{
if (prices[i] > prices[i - 1])
{
priceUpCount++;
}
if (dsmaValues[i] > dsmaValues[i - 1])
{
dsmaUpCount++;
}
}
// DSMA should have similar directional trend but smoother
// (fewer direction changes due to filtering)
Assert.True(Math.Abs(dsmaUpCount - priceUpCount) < prices.Count * 0.3,
$"DSMA direction changes {dsmaUpCount} should be reasonably aligned with price {priceUpCount}");
}
[Fact]
public void Dsma_MatchesOoples_Structural()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var ooplesData = bars.Select(b => new TickerData
{
Date = new DateTime(b.Time, DateTimeKind.Utc),
Open = b.Open, High = b.High, Low = b.Low,
Close = b.Close, Volume = b.Volume
}).ToList();
var result = new StockData(ooplesData).CalculateEhlersDeviationScaledMovingAverage();
var values = result.CustomValuesList;
int finiteCount = values.Count(v => double.IsFinite(v));
Assert.True(finiteCount > 100, $"Expected >100 finite values, got {finiteCount}");
}
}