Files
Miha Kralj 060649192f 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
2026-03-12 12:34:16 -07:00

359 lines
12 KiB
C#

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}");
}
}