mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 22:08:05 +00:00
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:
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user