mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 22:08:05 +00:00
SIMD Refactor: Merge simd-dev into dev (#55)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat> Co-authored-by: Warp <agent@warp.dev>
This commit is contained in:
co-authored by
Claude Opus 4.5
aider
Warp
parent
5bcdf8d614
commit
86fe32a682
@@ -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,69 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public class DsmaIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 2, 1000, 1, 0)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[InputParameter("Scale Factor", sortIndex: 2, 0.01, 0.9, 0.01, 2)]
|
||||
public double ScaleFactor { get; set; } = 0.5;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
[InputParameter("Color", sortIndex: 22)]
|
||||
public Color LineColor { get; set; } = IndicatorExtensions.Averages;
|
||||
|
||||
[InputParameter("Width", sortIndex: 23)]
|
||||
public int LineWidth { get; set; } = 2;
|
||||
|
||||
private Dsma ma = null!;
|
||||
protected LineSeries Series;
|
||||
protected string SourceName = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"DSMA {Period}:{ScaleFactor:F2}:{SourceName}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/trends_IIR/dsma/Dsma.Quantower.cs";
|
||||
|
||||
public DsmaIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
SourceName = Source.ToString();
|
||||
Name = "DSMA - Deviation-Scaled Moving Average";
|
||||
Description = "Deviation-Scaled Moving Average with Super Smoother filter";
|
||||
Series = new LineSeries(name: $"DSMA {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(Series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
ma = new Dsma(Period, ScaleFactor);
|
||||
SourceName = Source.ToString();
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
Series.Color = LineColor;
|
||||
Series.Width = LineWidth;
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
|
||||
|
||||
TValue result = ma.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), isNew: args.IsNewBar());
|
||||
|
||||
Series.SetValue(result.Value, ma.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -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.Calculate(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.Calculate(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.Calculate(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.Calculate(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.Calculate(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.Calculate(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,331 @@
|
||||
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}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Deviation-Scaled Moving Average (DSMA):
|
||||
/// An adaptive moving average that uses standard deviation to dynamically adjust
|
||||
/// its smoothing factor. Combines a 2-pole Super Smoother filter for trend estimation
|
||||
/// with RMS-based deviation scaling for volatility adaptation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Key characteristics:
|
||||
/// - Uses Super Smoother (Butterworth) 2-pole IIR filter for trend extraction
|
||||
/// - RMS (Root Mean Square) of filtered deviations for volatility measurement
|
||||
/// - Dynamic alpha scaling based on deviation ratio (|filtered| / RMS)
|
||||
/// - O(1) streaming updates via circular buffer for RMS calculation
|
||||
/// - Adapts smoothing: faster in trending markets, slower in ranging markets
|
||||
///
|
||||
/// Mathematical foundation:
|
||||
/// 1. Super Smoother: H(z) = c₁(1 + z⁻¹) / (1 - b₁z⁻¹ + a₁²z⁻²)
|
||||
/// where a₁ = exp(-√2·π/period), b₁ = 2a₁·cos(√2·π/period), c₁ = (1-b₁+a₁²)/2
|
||||
/// 2. RMS = √(Σ(filt²)/period)
|
||||
/// 3. alpha = min(scaleFactor · 5/period · |filt|/RMS, 1)
|
||||
/// 4. DSMA = alpha·price + (1-alpha)·prevDSMA
|
||||
///
|
||||
/// Performance:
|
||||
/// - Update: O(1) with FMA optimizations
|
||||
/// - Memory: O(period) for RMS buffer
|
||||
/// - SIMD: Calculate method uses vectorized RMS computation
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Dsma : AbstractBase
|
||||
{
|
||||
private const double SqrtTwo = 1.414213562373095;
|
||||
private const double ScaleMultiplier = 5.0;
|
||||
private const double MinRms = 1e-10;
|
||||
|
||||
// Super Smoother filter coefficients (precomputed from period)
|
||||
private readonly double _b1; // 2a₁·cos(√2·π/period)
|
||||
private readonly double _c1Half; // c₁/2 for optimization
|
||||
private readonly double _a1Sq; // a₁² for optimization
|
||||
|
||||
// RMS scaling parameters
|
||||
private readonly double _periodRecip; // 1/period
|
||||
private readonly double _scaleAdjustment; // scaleFactor · 5 / period
|
||||
|
||||
// Circular buffer for filtered deviations squared
|
||||
private readonly RingBuffer _filtSquaredBuffer;
|
||||
|
||||
// Event handler
|
||||
private readonly TValuePublishedHandler _handler;
|
||||
|
||||
// Streaming state (current + previous for isNew=false rollback)
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State
|
||||
{
|
||||
// Super Smoother filter state
|
||||
public double Filt; // current filtered value
|
||||
public double Filt1; // filt[t-1]
|
||||
public double Filt2; // filt[t-2]
|
||||
public double Zeros1; // (price - result)[t-1]
|
||||
|
||||
// RMS tracking
|
||||
public double SumSquared; // running sum of filtered² values
|
||||
|
||||
// Result tracking
|
||||
public double Result; // current DSMA value
|
||||
public double LastPrice; // last finite price (for NaN handling)
|
||||
|
||||
// Counter
|
||||
public int Bars;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicator is "hot" (warmed up) once we have at least Period bars.
|
||||
/// </summary>
|
||||
public override bool IsHot => _state.Bars >= WarmupPeriod;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new DSMA indicator with the specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="period">Lookback period for both trend filtering and RMS calculation (≥2)</param>
|
||||
/// <param name="scaleFactor">Combined scaling/smoothing factor (0.01-0.9). Higher = more responsive.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">If period < 2 or scaleFactor outside valid range</exception>
|
||||
public Dsma(int period, double scaleFactor = 0.5)
|
||||
{
|
||||
if (period < 2)
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 2.");
|
||||
if (scaleFactor < 0.01 || scaleFactor > 0.9)
|
||||
throw new ArgumentOutOfRangeException(nameof(scaleFactor), "Scale factor must be between 0.01 and 0.9.");
|
||||
|
||||
WarmupPeriod = period;
|
||||
_periodRecip = 1.0 / period;
|
||||
_scaleAdjustment = scaleFactor * ScaleMultiplier * _periodRecip;
|
||||
|
||||
// Precompute Super Smoother coefficients
|
||||
// a₁ = exp(-√2·π/(period/2)) = exp(-√2·π·2/period)
|
||||
double arg = SqrtTwo * Math.PI / (period * 0.5);
|
||||
double a1 = Math.Exp(-arg);
|
||||
_b1 = 2.0 * a1 * Math.Cos(arg);
|
||||
_a1Sq = a1 * a1;
|
||||
double c1 = 1.0 - _b1 + _a1Sq;
|
||||
_c1Half = c1 * 0.5;
|
||||
|
||||
_filtSquaredBuffer = new RingBuffer(period);
|
||||
_handler = Handle;
|
||||
Name = $"Dsma({period},{scaleFactor:F2})";
|
||||
|
||||
Reset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new DSMA indicator that subscribes to a source publisher.
|
||||
/// </summary>
|
||||
/// <param name="source">Source data publisher</param>
|
||||
/// <param name="period">Lookback period (≥2)</param>
|
||||
/// <param name="scaleFactor">Scaling factor (0.01-0.9)</param>
|
||||
public Dsma(ITValuePublisher source, int period, double scaleFactor = 0.5)
|
||||
: this(period, scaleFactor)
|
||||
{
|
||||
source.Pub += _handler;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void Reset()
|
||||
{
|
||||
_state = default;
|
||||
_p_state = default;
|
||||
_filtSquaredBuffer.Clear();
|
||||
Last = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core streaming step: processes a single input value and returns the DSMA result.
|
||||
/// </summary>
|
||||
/// <param name="value">Input price value</param>
|
||||
/// <param name="isNew">True for new bar, false for bar correction</param>
|
||||
/// <returns>DSMA value</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double Step(double value, bool isNew)
|
||||
{
|
||||
HandleStateSnapshot(isNew);
|
||||
value = HandleInvalidInput(value);
|
||||
if (double.IsNaN(value))
|
||||
return double.NaN;
|
||||
|
||||
_state.Bars++;
|
||||
|
||||
if (_state.Bars == 1)
|
||||
return InitializeFirstBar(value);
|
||||
|
||||
return CalculateDsma(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void HandleStateSnapshot(bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_p_state = _state;
|
||||
_filtSquaredBuffer.Snapshot();
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = _p_state;
|
||||
_filtSquaredBuffer.Restore();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double HandleInvalidInput(double value)
|
||||
{
|
||||
if (!double.IsFinite(value))
|
||||
{
|
||||
return _state.Bars == 0 ? double.NaN : _state.LastPrice;
|
||||
}
|
||||
|
||||
_state.LastPrice = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double InitializeFirstBar(double value)
|
||||
{
|
||||
_state.Result = value;
|
||||
_state.Filt = 0.0;
|
||||
_state.Filt1 = 0.0;
|
||||
_state.Filt2 = 0.0;
|
||||
_state.Zeros1 = 0.0;
|
||||
_state.SumSquared = 0.0;
|
||||
return value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double CalculateDsma(double value)
|
||||
{
|
||||
// 1. Calculate deviation from current estimate
|
||||
double zeros = value - _state.Result;
|
||||
|
||||
// 2. Apply Super Smoother (2-pole Butterworth) filter
|
||||
// filt = c₁/2 · (zeros + zeros[t-1]) + b₁·filt[t-1] - a₁²·filt[t-2]
|
||||
// Using FMA for the core computation
|
||||
double filtPart1 = _c1Half * (zeros + _state.Zeros1);
|
||||
double filtPart2 = Math.FusedMultiplyAdd(_state.Filt1, _b1, -_a1Sq * _state.Filt2);
|
||||
double filt = filtPart1 + filtPart2;
|
||||
|
||||
// 3. Update RMS tracking with filtered value squared
|
||||
double filtSq = filt * filt;
|
||||
double removed = _filtSquaredBuffer.Add(filtSq);
|
||||
_state.SumSquared = Math.FusedMultiplyAdd(-1.0, removed, _state.SumSquared + filtSq);
|
||||
|
||||
// 4. Calculate RMS from running sum
|
||||
double rms = Math.Sqrt(Math.Max(_state.SumSquared * _periodRecip, MinRms));
|
||||
|
||||
// 5. Compute adaptive alpha: scale by |filt|/RMS ratio
|
||||
double alpha = Math.Min(_scaleAdjustment * Math.Abs(filt / rms), 1.0);
|
||||
|
||||
// 6. Apply adaptive EMA: result = alpha·value + (1-alpha)·prevResult
|
||||
// Using FMA: result = prevResult·(1-alpha) + alpha·value
|
||||
double decay = 1.0 - alpha;
|
||||
double result = Math.FusedMultiplyAdd(_state.Result, decay, alpha * value);
|
||||
|
||||
// 7. Update state for next iteration
|
||||
_state.Zeros1 = zeros;
|
||||
_state.Filt2 = _state.Filt1;
|
||||
_state.Filt1 = filt;
|
||||
_state.Filt = filt;
|
||||
_state.Result = result;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a new value.
|
||||
/// </summary>
|
||||
/// <param name="input">Input value with timestamp</param>
|
||||
/// <param name="isNew">True for new bar, false for bar correction</param>
|
||||
/// <returns>Updated indicator value</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
double result = Step(input.Value, isNew);
|
||||
Last = new TValue(input.Time, result);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch processes a time series and returns the DSMA results.
|
||||
/// </summary>
|
||||
/// <param name="source">Source time series</param>
|
||||
/// <returns>Time series containing DSMA values</returns>
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0) return [];
|
||||
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
|
||||
source.Times.CopyTo(tSpan);
|
||||
|
||||
Reset();
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
vSpan[i] = Step(source.Values[i], isNew: true);
|
||||
}
|
||||
|
||||
// Synchronize state for subsequent streaming calls
|
||||
_p_state = _state;
|
||||
_filtSquaredBuffer.Snapshot();
|
||||
|
||||
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
private void Handle(object? sender, in TValueEventArgs args) => Update(args.Value, args.IsNew);
|
||||
|
||||
/// <summary>
|
||||
/// Primes the indicator with historical data.
|
||||
/// </summary>
|
||||
/// <param name="source">Historical price data</param>
|
||||
/// <param name="step">Optional time step (not used in calculation)</param>
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
foreach (var value in source)
|
||||
{
|
||||
Update(new TValue(DateTime.MinValue, value));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculates DSMA for a time series.
|
||||
/// </summary>
|
||||
/// <param name="source">Source time series</param>
|
||||
/// <param name="period">Lookback period (≥2)</param>
|
||||
/// <param name="scaleFactor">Scaling factor (0.01-0.9)</param>
|
||||
/// <returns>Time series containing DSMA values</returns>
|
||||
public static TSeries Batch(TSeries source, int period, double scaleFactor = 0.5)
|
||||
{
|
||||
var dsma = new Dsma(period, scaleFactor);
|
||||
return dsma.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates DSMA for a span of values.
|
||||
/// </summary>
|
||||
/// <param name="source">Source data span</param>
|
||||
/// <param name="output">Output span (must be at least as long as source)</param>
|
||||
/// <param name="period">Lookback period (≥2)</param>
|
||||
/// <param name="scaleFactor">Scaling factor (0.01-0.9)</param>
|
||||
/// <exception cref="ArgumentException">If output span is shorter than source</exception>
|
||||
public static void Calculate(ReadOnlySpan<double> source,
|
||||
Span<double> output,
|
||||
int period,
|
||||
double scaleFactor = 0.5)
|
||||
{
|
||||
if (output.Length < source.Length)
|
||||
throw new ArgumentException("Output span is shorter than source span.", nameof(output));
|
||||
|
||||
var dsma = new Dsma(period, scaleFactor);
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
output[i] = dsma.Step(source[i], isNew: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
# DSMA: Deviation-Scaled Moving Average
|
||||
|
||||
> "When the market screams, DSMA sprints. When it whispers, DSMA crawls. An adaptive moving average that lets volatility dictate the pace."
|
||||
|
||||
DSMA (Deviation-Scaled Moving Average) is a volatility-adaptive trend filter that combines a Super Smoother (2-pole Butterworth IIR filter) with RMS-based deviation scaling. Unlike fixed-period moving averages that treat all market conditions identically, DSMA adjusts its responsiveness based on measured volatility—accelerating when trends are strong and decelerating when prices consolidate.
|
||||
|
||||
## Historical Context
|
||||
|
||||
DSMA appears to be a proprietary or boutique indicator without mainstream adoption in commercial platforms. The algorithm surfaced in custom PineScript implementations, drawing from established signal processing concepts: Butterworth filtering for trend extraction (popularized by John Ehlers) and RMS deviation measurement for volatility assessment. This QuanTAlib implementation follows the PineScript reference, translating its recursive logic into high-performance C# with zero-allocation streaming updates.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
DSMA operates in three stages, each addressing a specific signal processing challenge:
|
||||
|
||||
### Stage 1: Trend Extraction via Super Smoother
|
||||
|
||||
The Super Smoother is a 2-pole Butterworth low-pass filter—the same topology used in analog audio circuits to eliminate high-frequency noise without phase distortion. Ehlers adapted it for financial time series by discretizing the transfer function:
|
||||
|
||||
$$ H(z) = \frac{c_0 + c_1 z^{-1} + c_2 z^{-2}}{1 - a_1 z^{-1} - a_2 z^{-2}} $$
|
||||
|
||||
Coefficients are precomputed from the period parameter:
|
||||
|
||||
$$ \omega = \frac{\sqrt{2} \cdot \pi}{\text{period}} $$
|
||||
|
||||
$$ a = e^{-\omega} $$
|
||||
|
||||
$$ c_0 = \frac{(1 - a)^2}{1 + 2a \cos(\omega) + a^2} $$
|
||||
|
||||
The filter maintains two delay states ($z^{-1}$, $z^{-2}$) and produces a smooth baseline trend ($\text{filt}_t$) with minimal lag for its degree of smoothing.
|
||||
|
||||
### Stage 2: Volatility Measurement via RMS
|
||||
|
||||
Root Mean Square (RMS) quantifies the magnitude of oscillations around the filtered trend:
|
||||
|
||||
$$ \text{RMS}_t = \sqrt{\frac{1}{N} \sum_{i=0}^{N-1} (\text{price}_{t-i} - \text{filt}_{t-i})^2} $$
|
||||
|
||||
RMS is computed incrementally over a rolling window using a circular `RingBuffer` for O(1) updates. Unlike standard deviation (which measures dispersion around a mean), RMS measures absolute deviation from the trend line—a more direct proxy for volatility in trend-following contexts.
|
||||
|
||||
### Stage 3: Adaptive Alpha Scaling
|
||||
|
||||
The final EMA-style smoothing coefficient adapts based on the ratio of trend strength to volatility:
|
||||
|
||||
$$ \alpha_t = \min\left(\text{scaleFactor} \cdot \frac{5}{\text{period}} \cdot \frac{|\text{filt}_t|}{\text{RMS}_t}, 1\right) $$
|
||||
|
||||
- **Numerator** ($|\text{filt}_t|$): Captures the magnitude of the filtered deviation.
|
||||
- **Denominator** ($\text{RMS}_t$): Normalizes by recent volatility, preventing over-reaction to noise.
|
||||
- **Scale Factor**: User-adjustable multiplier (default 0.5) to control overall responsiveness.
|
||||
- **Clamping**: Alpha is bounded at 1.0 to prevent numerical instability.
|
||||
|
||||
When trends are strong relative to volatility (high signal-to-noise ratio), alpha approaches its maximum, and DSMA tracks price aggressively. During consolidation (low signal-to-noise), alpha shrinks, and DSMA smooths heavily.
|
||||
|
||||
The final output is an exponential moving average using this dynamic alpha:
|
||||
|
||||
$$ \text{DSMA}_t = \alpha_t \cdot \text{price}_t + (1 - \alpha_t) \cdot \text{DSMA}_{t-1} $$
|
||||
|
||||
Implemented with fused multiply-add for single-rounding precision:
|
||||
|
||||
```csharp
|
||||
_state.Dsma = Math.FusedMultiplyAdd(_state.Dsma, 1.0 - alpha, alpha * input.Value);
|
||||
```
|
||||
|
||||
## Performance Profile
|
||||
|
||||
DSMA combines the computational cost of a 2-pole IIR filter, a rolling RMS calculation, and an EMA update—still achieving constant-time complexity through incremental ring buffer updates.
|
||||
|
||||
### Operation Count (Streaming Mode, Scalar)
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| **Stage 1: Deviation Calculation** | | | |
|
||||
| SUB (zeros = value - result) | 1 | 1 | 1 |
|
||||
| **Stage 2: Super Smoother (2-pole Butterworth)** | | | |
|
||||
| ADD (zeros + zeros1) | 1 | 1 | 1 |
|
||||
| MUL (c1Half × sum) | 1 | 3 | 3 |
|
||||
| FMA (filt1 × b1 - a1Sq × filt2) | 1 | 4 | 4 |
|
||||
| MUL (a1Sq × filt2) | 1 | 3 | 3 |
|
||||
| ADD (filtPart1 + filtPart2) | 1 | 1 | 1 |
|
||||
| **Stage 3: RMS Buffer Update** | | | |
|
||||
| MUL (filt × filt) | 1 | 3 | 3 |
|
||||
| ADD/SUB (sumSquared update) | 2 | 1 | 2 |
|
||||
| FMA (running sum) | 1 | 4 | 4 |
|
||||
| **Stage 4: RMS Calculation** | | | |
|
||||
| MUL (sumSquared × periodRecip) | 1 | 3 | 3 |
|
||||
| CMP/MAX (MinRms guard) | 1 | 1 | 1 |
|
||||
| SQRT | 1 | 15 | 15 |
|
||||
| **Stage 5: Alpha Calculation** | | | |
|
||||
| ABS | 1 | 1 | 1 |
|
||||
| DIV (filt / rms) | 1 | 15 | 15 |
|
||||
| MUL (scaleAdjustment × ratio) | 1 | 3 | 3 |
|
||||
| CMP/MIN (clamp to 1.0) | 1 | 1 | 1 |
|
||||
| **Stage 6: Adaptive EMA** | | | |
|
||||
| SUB (1 - alpha) | 1 | 1 | 1 |
|
||||
| FMA (result × decay + alpha × value) | 1 | 4 | 4 |
|
||||
| MUL (alpha × value) | 1 | 3 | 3 |
|
||||
| **Total** | | | **~69 cycles** |
|
||||
|
||||
**Dominant costs:**
|
||||
- SQRT (15 cycles, 22%) — RMS calculation
|
||||
- DIV (15 cycles, 22%) — alpha normalization by RMS
|
||||
- Super Smoother filter (~12 cycles, 17%) — 2-pole IIR recursion
|
||||
|
||||
### Batch Mode (SIMD Analysis)
|
||||
|
||||
DSMA is **not SIMD-parallelizable** across bars due to:
|
||||
1. Super Smoother is a 2-pole IIR filter with recursive state (filt[t-1], filt[t-2])
|
||||
2. Adaptive alpha depends on current RMS which depends on running sum
|
||||
3. Final EMA output feeds back as input to next iteration
|
||||
|
||||
**FMA optimization (already applied):**
|
||||
- RMS running sum update uses FMA
|
||||
- Final adaptive EMA uses FMA pattern
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Accuracy** | 7/10 | Volatile in choppy markets; shines in trends |
|
||||
| **Timeliness** | 8/10 | Adaptive lag—minimal during strong trends |
|
||||
| **Overshoot** | 6/10 | Can overshoot when volatility spikes suddenly |
|
||||
| **Smoothness** | 8/10 | Super Smoother baseline ensures good filtering |
|
||||
|
||||
*Benchmarked on Intel i7-12700K @ 3.6 GHz (Turbo off), AVX2, .NET 10.0, 10K iterations.*
|
||||
|
||||
## Validation
|
||||
|
||||
DSMA is not implemented in mainstream libraries (TA-Lib, Skender, Tulip, Ooples). Validation relies on behavioral testing against known algorithm properties.
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **TA-Lib** | N/A | Not implemented |
|
||||
| **Skender** | N/A | Not implemented |
|
||||
| **Tulip** | N/A | Not implemented |
|
||||
| **Ooples** | N/A | Not implemented |
|
||||
| **Behavioral** | ✅ | Validated: trend following, volatility response, bounds |
|
||||
|
||||
### Behavioral Test Summary
|
||||
|
||||
- **Trend Following**: DSMA converges to price during sustained trends (10 consecutive bars ±1%, final deviation <2%)
|
||||
- **Volatility Response**: Higher scale factor (0.9 vs 0.1) produces 3-5x larger deviation during volatile periods
|
||||
- **Smoothness**: Output exhibits <50% of input variance when volatility is low (validated over 1000 GBM bars)
|
||||
- **Bounds**: Output remains within [min, max] price range ±1% tolerance
|
||||
- **Mathematical Consistency**: Streaming updates match batch calculations (ε < 1e-10)
|
||||
|
||||
## C# Implementation Considerations
|
||||
|
||||
### State Management
|
||||
|
||||
DSMA uses a comprehensive State record struct combining all filter stages:
|
||||
|
||||
```csharp
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State
|
||||
{
|
||||
public double Filt; // current filtered value
|
||||
public double Filt1; // filt[t-1]
|
||||
public double Filt2; // filt[t-2]
|
||||
public double Zeros1; // deviation[t-1]
|
||||
public double SumSquared; // running sum for RMS
|
||||
public double Result; // current DSMA value
|
||||
public double LastPrice; // last valid price
|
||||
public int Bars;
|
||||
}
|
||||
```
|
||||
|
||||
Bar correction requires coordinated rollback of both state and RingBuffer:
|
||||
|
||||
```csharp
|
||||
if (isNew) { _p_state = _state; _filtSquaredBuffer.Snapshot(); }
|
||||
else { _state = _p_state; _filtSquaredBuffer.Restore(); }
|
||||
```
|
||||
|
||||
### RingBuffer for RMS
|
||||
|
||||
The RingBuffer maintains O(1) running sum updates for RMS calculation:
|
||||
|
||||
```csharp
|
||||
double removed = _filtSquaredBuffer.Add(filtSq);
|
||||
_state.SumSquared = Math.FusedMultiplyAdd(-1.0, removed, _state.SumSquared + filtSq);
|
||||
```
|
||||
|
||||
The buffer's `Snapshot()`/`Restore()` methods enable atomic rollback on bar corrections.
|
||||
|
||||
### Precomputed Constants
|
||||
|
||||
Constructor calculates all filter coefficients once:
|
||||
|
||||
```csharp
|
||||
double arg = SqrtTwo * Math.PI / (period * 0.5);
|
||||
double a1 = Math.Exp(-arg);
|
||||
_b1 = 2.0 * a1 * Math.Cos(arg);
|
||||
_a1Sq = a1 * a1;
|
||||
_c1Half = (1.0 - _b1 + _a1Sq) * 0.5;
|
||||
_periodRecip = 1.0 / period;
|
||||
_scaleAdjustment = scaleFactor * 5.0 / period;
|
||||
```
|
||||
|
||||
### FMA Usage
|
||||
|
||||
FMA optimizes the Super Smoother IIR and adaptive EMA:
|
||||
|
||||
```csharp
|
||||
// Super Smoother: filt = c1Half*(zeros+zeros1) + b1*filt1 - a1Sq*filt2
|
||||
double filtPart2 = Math.FusedMultiplyAdd(_state.Filt1, _b1, -_a1Sq * _state.Filt2);
|
||||
|
||||
// Adaptive EMA: result = prevResult*decay + alpha*value
|
||||
double result = Math.FusedMultiplyAdd(_state.Result, decay, alpha * value);
|
||||
```
|
||||
|
||||
### Memory Layout
|
||||
|
||||
| Field | Type | Size | Purpose |
|
||||
| :--- | :--- | :---: | :--- |
|
||||
| `_b1` | double | 8B | Super Smoother coefficient |
|
||||
| `_c1Half` | double | 8B | Halved c₁ coefficient |
|
||||
| `_a1Sq` | double | 8B | a₁² coefficient |
|
||||
| `_periodRecip` | double | 8B | 1/period |
|
||||
| `_scaleAdjustment` | double | 8B | Combined scale factor |
|
||||
| `_filtSquaredBuffer` | RingBuffer | ~8B+period×8B | Circular buffer for RMS |
|
||||
| `_state` | State | ~64B | Current calculation state |
|
||||
| `_p_state` | State | ~64B | Previous state for rollback |
|
||||
| **Total (fixed)** | | **~176B + period×8B** | Per indicator instance |
|
||||
|
||||
### SIMD Limitations
|
||||
|
||||
The 2-pole IIR recursion and adaptive alpha dependency on running RMS preclude SIMD parallelization across bars. The `Calculate(Span)` method uses a scalar loop—parallelization should target multiple independent series rather than within-series vectorization.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Warmup Period**: DSMA requires `Period` bars to fill the Super Smoother delay line and RMS buffer. The first `Period` outputs will be unstable. Use `IsHot` to detect when the indicator has sufficient history.
|
||||
|
||||
2. **Scale Factor Sensitivity**: The default `scaleFactor = 0.5` balances responsiveness and stability. Values >0.7 can cause whipsaws in choppy markets; values <0.3 introduce excessive lag. Tune this parameter based on your asset's typical volatility regime.
|
||||
|
||||
3. **Volatility Normalization**: The RMS denominator in the alpha formula can approach zero during extended flat periods, causing alpha to spike. The implementation clamps alpha at 1.0, but extremely low volatility can still produce jittery behavior. Consider a minimum RMS threshold (not implemented in this version).
|
||||
|
||||
4. **Not a Momentum Oscillator**: DSMA is a trend filter, not a momentum indicator. Do not confuse high alpha values with strong momentum—alpha reflects signal-to-noise ratio, not directional strength. Use a separate momentum indicator (RSI, MACD) for confirmation.
|
||||
|
||||
5. **Comparison with JMA**: DSMA uses a simpler adaptive mechanism than JMA (which employs fractal efficiency and phase adjustment). JMA typically offers smoother output and better overshoot control but at higher computational cost. DSMA is faster and more transparent algorithmically.
|
||||
|
||||
6. **Bar Correction**: Like all QuanTAlib indicators, DSMA supports bar correction via the `isNew` parameter. When `isNew = false`, it rolls back to the previous state before recalculating. Ensure your data feed correctly signals bar updates versus corrections.
|
||||
|
||||
7. **SIMD Limitation**: The recursive nature of the Super Smoother filter and adaptive alpha calculation precludes efficient SIMD vectorization. The `Calculate(Span)` method uses a scalar loop. For bulk backtesting, consider parallelizing across multiple series rather than within a single series.
|
||||
@@ -0,0 +1,59 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Deviation-Scaled Moving Average (DSMA)", "DSMA", overlay=true)
|
||||
|
||||
//@function Calculates DSMA using standard deviation to scale the averaging factor
|
||||
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/trends_IIR/dsma.md
|
||||
//@param source Series to calculate DSMA from
|
||||
//@param period Length of the lookback period for both average and deviation calculation
|
||||
//@param scaleFactor Combined scaling/smoothing factor (0.01-0.9)
|
||||
//@returns DSMA value that adapts to market volatility through deviation scaling
|
||||
//@optimized Uses circular buffer for RMS calculation and adaptive alpha for O(1) complexity
|
||||
dsma(series float source, simple int period, simple float scaleFactor=0.5) =>
|
||||
float a1 = math.exp(-1.414 * math.pi / (period * 0.5))
|
||||
float b1 = 2.0 * a1 * math.cos(1.414 * math.pi / (period * 0.5))
|
||||
float c1 = 1.0 - b1 + (a1 * a1)
|
||||
float c1Half = c1 * 0.5
|
||||
float periodRecip = 1.0 / period
|
||||
float scaleAdjustment = scaleFactor * 5.0 * periodRecip
|
||||
var float result = na
|
||||
var float filt = 0.0
|
||||
var float filt1 = 0.0
|
||||
var float filt2 = 0.0
|
||||
var float zeros1 = 0.0
|
||||
var float sumSquared = 0.0
|
||||
var array<float> filtSquared = array.new_float(period, 0.0)
|
||||
var int bufferIndex = 0
|
||||
if na(source)
|
||||
result
|
||||
else
|
||||
if na(result)
|
||||
result := source
|
||||
else
|
||||
float zeros = source - result
|
||||
filt := c1Half * (zeros + zeros1) + b1 * filt1 - (a1 * a1) * filt2
|
||||
float filtSq = filt * filt
|
||||
sumSquared := sumSquared + filtSq - array.get(filtSquared, bufferIndex)
|
||||
array.set(filtSquared, bufferIndex, filtSq)
|
||||
bufferIndex := (bufferIndex + 1) % period
|
||||
float rms = math.sqrt(math.max(sumSquared * periodRecip, 1e-10))
|
||||
float alpha = math.min(scaleAdjustment * math.abs(filt / rms), 1.0)
|
||||
result := alpha * source + (1 - alpha) * result
|
||||
zeros1 := zeros
|
||||
filt2 := filt1
|
||||
filt1 := filt
|
||||
result
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_source = input.source(close, "Source")
|
||||
i_period = input.int(25, "Period", minval=2)
|
||||
i_scale = input.float(0.9, "Scale Factor", minval=0.01, maxval=0.9, step=0.01)
|
||||
|
||||
// Calculation
|
||||
dsma_value = dsma(i_source, i_period, i_scale)
|
||||
|
||||
// Plot
|
||||
plot(dsma_value, "DSMA", color=color.yellow, linewidth=2)
|
||||
Reference in New Issue
Block a user