mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-12 23:58:04 +00:00
- Implemented the TRAMA (Trend Regularity Adaptive Moving Average) class with adaptive EMA logic. - Added unit tests for TRAMA functionality, including constructor validation, basic calculations, state management, and robustness checks. - Created validation tests to ensure consistency across different modes of operation (streaming, batch, and static calculations). - Enhanced documentation for TRAMA, including performance profiles and quality metrics. - Updated workspace configuration by removing unnecessary folder references.
564 lines
16 KiB
C#
564 lines
16 KiB
C#
namespace QuanTAlib.Tests;
|
|
|
|
public class SwmaTests
|
|
{
|
|
private static TSeries MakeSeries(int count = 500)
|
|
{
|
|
var gbm = new GBM(startPrice: 100, seed: 42);
|
|
var series = new TSeries();
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
series.Add(gbm.Next());
|
|
}
|
|
return series;
|
|
}
|
|
|
|
// === A) Constructor validation ===
|
|
|
|
[Fact]
|
|
public void Constructor_DefaultPeriod_Is4()
|
|
{
|
|
var swma = new Swma();
|
|
Assert.Equal("Swma(4)", swma.Name);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_CustomPeriod_SetsCorrectly()
|
|
{
|
|
var swma = new Swma(period: 10);
|
|
Assert.Equal("Swma(10)", swma.Name);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_Period2_IsValid()
|
|
{
|
|
var swma = new Swma(period: 2);
|
|
Assert.Equal("Swma(2)", swma.Name);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_PeriodBelow2_Throws()
|
|
{
|
|
var ex = Assert.Throws<ArgumentException>(() => new Swma(period: 1));
|
|
Assert.Equal("period", ex.ParamName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_PeriodZero_Throws()
|
|
{
|
|
var ex = Assert.Throws<ArgumentException>(() => new Swma(period: 0));
|
|
Assert.Equal("period", ex.ParamName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_NegativePeriod_Throws()
|
|
{
|
|
var ex = Assert.Throws<ArgumentException>(() => new Swma(period: -5));
|
|
Assert.Equal("period", ex.ParamName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_SetsWarmupPeriod()
|
|
{
|
|
var swma = new Swma(period: 8);
|
|
Assert.Equal(8, swma.WarmupPeriod);
|
|
}
|
|
|
|
// === B) Basic calculation ===
|
|
|
|
[Fact]
|
|
public void Update_ReturnsTValue()
|
|
{
|
|
var swma = new Swma(period: 4);
|
|
var result = swma.Update(new TValue(DateTime.UtcNow, 100.0));
|
|
Assert.True(double.IsFinite(result.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_Last_IsAccessible()
|
|
{
|
|
var swma = new Swma(period: 4);
|
|
swma.Update(new TValue(DateTime.UtcNow, 100.0));
|
|
Assert.True(double.IsFinite(swma.Last.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_ConstantInput_ReturnsConstant()
|
|
{
|
|
var swma = new Swma(period: 4);
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
swma.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 50.0));
|
|
}
|
|
Assert.Equal(50.0, swma.Last.Value, 1e-10);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_Period4_KnownWeights_MatchesPine()
|
|
{
|
|
// PineScript ta.swma: period=4, weights [1,2,2,1]/6
|
|
var swma = new Swma(period: 4);
|
|
double[] vals = { 10, 20, 30, 40 };
|
|
for (int i = 0; i < vals.Length; i++)
|
|
{
|
|
swma.Update(new TValue(DateTime.UtcNow.AddSeconds(i), vals[i]));
|
|
}
|
|
// Expected: (1*10 + 2*20 + 2*30 + 1*40) / 6 = (10+40+60+40)/6 = 150/6 = 25.0
|
|
Assert.Equal(25.0, swma.Last.Value, 1e-10);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_Period3_KnownWeights()
|
|
{
|
|
// Period=3: half=1.0, weights: w(0)=1+1-|0-1|=1, w(1)=1+1-0=2, w(2)=1+1-|2-1|=1 => [1,2,1]/4
|
|
var swma = new Swma(period: 3);
|
|
double[] vals = { 10, 20, 30 };
|
|
for (int i = 0; i < vals.Length; i++)
|
|
{
|
|
swma.Update(new TValue(DateTime.UtcNow.AddSeconds(i), vals[i]));
|
|
}
|
|
// Expected: (1*10 + 2*20 + 1*30) / 4 = (10+40+30)/4 = 80/4 = 20.0
|
|
Assert.Equal(20.0, swma.Last.Value, 1e-10);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_Period2_KnownWeights()
|
|
{
|
|
// Period=2: half=0.5, weights: w(0)=0.5+1-|0-0.5|=1.0, w(1)=0.5+1-|1-0.5|=1.0 => [1,1]/2
|
|
var swma = new Swma(period: 2);
|
|
double[] vals = { 10, 20 };
|
|
for (int i = 0; i < vals.Length; i++)
|
|
{
|
|
swma.Update(new TValue(DateTime.UtcNow.AddSeconds(i), vals[i]));
|
|
}
|
|
// Expected: (1*10 + 1*20) / 2 = 15.0 (same as SMA)
|
|
Assert.Equal(15.0, swma.Last.Value, 1e-10);
|
|
}
|
|
|
|
// === C) State + bar correction ===
|
|
|
|
[Fact]
|
|
public void Update_IsNew_True_AdvancesState()
|
|
{
|
|
var swma = new Swma(period: 4);
|
|
swma.Update(new TValue(DateTime.UtcNow, 100.0), isNew: true);
|
|
swma.Update(new TValue(DateTime.UtcNow.AddSeconds(1), 110.0), isNew: true);
|
|
var r1 = swma.Last;
|
|
// New value should advance
|
|
swma.Update(new TValue(DateTime.UtcNow.AddSeconds(2), 120.0), isNew: true);
|
|
Assert.NotEqual(r1.Value, swma.Last.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_IsNew_False_Rewrites()
|
|
{
|
|
var swma = new Swma(period: 4);
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
swma.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i), isNew: true);
|
|
}
|
|
var afterNew = swma.Last;
|
|
|
|
// Correction with same value should return same result
|
|
swma.Update(new TValue(DateTime.UtcNow.AddSeconds(4), 104.0), isNew: false);
|
|
Assert.Equal(afterNew.Value, swma.Last.Value, 1e-10);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_IterativeCorrections_Restore()
|
|
{
|
|
var swma = new Swma(period: 4);
|
|
var gbm = new GBM(startPrice: 100, seed: 42);
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
swma.Update(gbm.Next(), isNew: true);
|
|
}
|
|
var baseline = swma.Last;
|
|
|
|
// Apply multiple corrections
|
|
swma.Update(new TValue(DateTime.UtcNow, 999.0), isNew: false);
|
|
swma.Update(new TValue(DateTime.UtcNow, 888.0), isNew: false);
|
|
swma.Update(new TValue(DateTime.UtcNow, 777.0), isNew: false);
|
|
|
|
// Restore with isNew=false using original value
|
|
swma.Update(new TValue(baseline.Time, baseline.Value), isNew: false);
|
|
|
|
// State should be preserved across corrections (buffer not mutated)
|
|
Assert.True(double.IsFinite(swma.Last.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Reset_ClearsState()
|
|
{
|
|
var swma = new Swma(period: 4);
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
swma.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i));
|
|
}
|
|
Assert.True(swma.IsHot);
|
|
|
|
swma.Reset();
|
|
Assert.False(swma.IsHot);
|
|
Assert.Equal(default, swma.Last);
|
|
}
|
|
|
|
// === D) Warmup/convergence ===
|
|
|
|
[Fact]
|
|
public void IsHot_FlipsAtPeriod()
|
|
{
|
|
var swma = new Swma(period: 5);
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
swma.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i));
|
|
Assert.False(swma.IsHot);
|
|
}
|
|
swma.Update(new TValue(DateTime.UtcNow.AddSeconds(4), 104.0));
|
|
Assert.True(swma.IsHot);
|
|
}
|
|
|
|
[Fact]
|
|
public void WarmupPeriod_MatchesPeriod()
|
|
{
|
|
var swma = new Swma(period: 7);
|
|
Assert.Equal(7, swma.WarmupPeriod);
|
|
}
|
|
|
|
[Fact]
|
|
public void DuringWarmup_ReturnsRawValue()
|
|
{
|
|
var swma = new Swma(period: 5);
|
|
var result = swma.Update(new TValue(DateTime.UtcNow, 42.0));
|
|
Assert.Equal(42.0, result.Value, 1e-10);
|
|
}
|
|
|
|
// === E) Robustness ===
|
|
|
|
[Fact]
|
|
public void Update_NaN_UsesLastValid()
|
|
{
|
|
var swma = new Swma(period: 4);
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
swma.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0));
|
|
}
|
|
|
|
|
|
swma.Update(new TValue(DateTime.UtcNow.AddSeconds(5), double.NaN));
|
|
// After NaN, last-valid substitution should produce finite result
|
|
Assert.True(double.IsFinite(swma.Last.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_Infinity_UsesLastValid()
|
|
{
|
|
var swma = new Swma(period: 4);
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
swma.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0));
|
|
}
|
|
|
|
swma.Update(new TValue(DateTime.UtcNow.AddSeconds(5), double.PositiveInfinity));
|
|
Assert.True(double.IsFinite(swma.Last.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_NegativeInfinity_UsesLastValid()
|
|
{
|
|
var swma = new Swma(period: 4);
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
swma.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0));
|
|
}
|
|
|
|
swma.Update(new TValue(DateTime.UtcNow.AddSeconds(5), double.NegativeInfinity));
|
|
Assert.True(double.IsFinite(swma.Last.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_FirstValueNaN_ReturnsNaN()
|
|
{
|
|
var swma = new Swma(period: 4);
|
|
var result = swma.Update(new TValue(DateTime.UtcNow, double.NaN));
|
|
Assert.True(double.IsNaN(result.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Batch_BatchNaN_Safe()
|
|
{
|
|
double[] source = { 10, 20, double.NaN, 40, 50, 60 };
|
|
double[] output = new double[source.Length];
|
|
Swma.Batch(source, output, period: 3);
|
|
|
|
for (int i = 0; i < output.Length; i++)
|
|
{
|
|
Assert.True(double.IsFinite(output[i]), $"output[{i}] should be finite");
|
|
}
|
|
}
|
|
|
|
// === F) Consistency (4 modes match) ===
|
|
|
|
[Fact]
|
|
public void AllModes_ProduceSameResults()
|
|
{
|
|
var src = MakeSeries(100);
|
|
int period = 6;
|
|
|
|
// Mode 1: Streaming
|
|
var streaming = new Swma(period);
|
|
var streamResults = new List<double>();
|
|
for (int i = 0; i < src.Count; i++)
|
|
{
|
|
streamResults.Add(streaming.Update(src[i]).Value);
|
|
}
|
|
|
|
// Mode 2: Batch TSeries
|
|
var batchResults = Swma.Batch(src, period);
|
|
|
|
// Mode 3: Span API
|
|
var spanOutput = new double[src.Count];
|
|
Swma.Batch(src.Values, spanOutput, period);
|
|
|
|
// Mode 4: Event-based
|
|
var publisher = new TSeries();
|
|
var eventResults = new List<double>();
|
|
var eventSwma = new Swma(publisher, period);
|
|
eventSwma.Pub += (object? sender, in TValueEventArgs e) => eventResults.Add(e.Value.Value);
|
|
for (int i = 0; i < src.Count; i++)
|
|
{
|
|
publisher.Add(src[i]);
|
|
}
|
|
|
|
// Compare all modes
|
|
Assert.Equal(src.Count, batchResults.Count);
|
|
Assert.Equal(src.Count, eventResults.Count);
|
|
|
|
for (int i = 0; i < src.Count; i++)
|
|
{
|
|
double s = streamResults[i];
|
|
double b = batchResults[i].Value;
|
|
double sp = spanOutput[i];
|
|
double ev = eventResults[i];
|
|
|
|
if (double.IsNaN(s))
|
|
{
|
|
Assert.True(double.IsNaN(b), $"batch[{i}] should be NaN");
|
|
Assert.True(double.IsNaN(sp), $"span[{i}] should be NaN");
|
|
Assert.True(double.IsNaN(ev), $"event[{i}] should be NaN");
|
|
}
|
|
else
|
|
{
|
|
Assert.Equal(s, b, 1e-10);
|
|
Assert.Equal(s, sp, 1e-10);
|
|
Assert.Equal(s, ev, 1e-10);
|
|
}
|
|
}
|
|
}
|
|
|
|
// === G) Span API tests ===
|
|
|
|
[Fact]
|
|
public void Batch_Span_MismatchedLengths_Throws()
|
|
{
|
|
double[] source = { 1, 2, 3 };
|
|
double[] output = new double[2];
|
|
|
|
var ex = Assert.Throws<ArgumentException>(() => Swma.Batch(source, output, period: 2));
|
|
Assert.Equal("output", ex.ParamName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Batch_Span_PeriodBelow2_Throws()
|
|
{
|
|
double[] source = { 1, 2, 3 };
|
|
double[] output = new double[3];
|
|
|
|
var ex = Assert.Throws<ArgumentException>(() => Swma.Batch(source, output, period: 1));
|
|
Assert.Equal("period", ex.ParamName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Batch_Span_EmptyInput_NoOutput()
|
|
{
|
|
Swma.Batch(ReadOnlySpan<double>.Empty, Span<double>.Empty, period: 4);
|
|
Assert.True(true); // No exception = pass
|
|
}
|
|
|
|
[Fact]
|
|
public void Batch_Span_MatchesTSeries()
|
|
{
|
|
var src = MakeSeries(200);
|
|
int period = 5;
|
|
|
|
var tsResult = Swma.Batch(src, period);
|
|
var spanOutput = new double[src.Count];
|
|
Swma.Batch(src.Values, spanOutput, period);
|
|
|
|
for (int i = 0; i < src.Count; i++)
|
|
{
|
|
Assert.Equal(tsResult[i].Value, spanOutput[i], 1e-10);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Batch_Span_NaN_HandledGracefully()
|
|
{
|
|
double[] source = { 10, double.NaN, 30, 40, 50 };
|
|
double[] output = new double[5];
|
|
|
|
Swma.Batch(source, output, period: 3);
|
|
|
|
// After NaN substitution, all outputs should be finite
|
|
for (int i = 0; i < output.Length; i++)
|
|
{
|
|
Assert.True(double.IsFinite(output[i]), $"output[{i}] should be finite");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Batch_Span_LargeData_NoStackOverflow()
|
|
{
|
|
int count = 10_000;
|
|
double[] source = new double[count];
|
|
double[] output = new double[count];
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
source[i] = 100.0 + (i % 50);
|
|
}
|
|
|
|
Swma.Batch(source, output, period: 20);
|
|
|
|
Assert.True(double.IsFinite(output[^1]));
|
|
}
|
|
|
|
// === H) Chainability ===
|
|
|
|
[Fact]
|
|
public void Pub_FiresOnUpdate()
|
|
{
|
|
var swma = new Swma(period: 4);
|
|
int pubCount = 0;
|
|
swma.Pub += (object? sender, in TValueEventArgs e) => pubCount++;
|
|
|
|
swma.Update(new TValue(DateTime.UtcNow, 100.0));
|
|
Assert.Equal(1, pubCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void EventBased_Chaining_Works()
|
|
{
|
|
var publisher = new TSeries();
|
|
var swma = new Swma(publisher, period: 4);
|
|
int resultCount = 0;
|
|
swma.Pub += (object? sender, in TValueEventArgs e) => resultCount++;
|
|
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
publisher.Add(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i));
|
|
}
|
|
Assert.Equal(10, resultCount);
|
|
}
|
|
|
|
// === Additional: Calculate API ===
|
|
|
|
[Fact]
|
|
public void Calculate_ReturnsResultsAndIndicator()
|
|
{
|
|
var src = MakeSeries(50);
|
|
var (results, indicator) = Swma.Calculate(src, period: 5);
|
|
|
|
Assert.Equal(50, results.Count);
|
|
Assert.True(indicator.IsHot);
|
|
}
|
|
|
|
// === Dispose ===
|
|
|
|
[Fact]
|
|
public void Dispose_UnsubscribesFromSource()
|
|
{
|
|
var publisher = new TSeries();
|
|
var swma = new Swma(publisher, period: 4);
|
|
int pubCount = 0;
|
|
swma.Pub += (object? sender, in TValueEventArgs e) => pubCount++;
|
|
|
|
publisher.Add(new TValue(DateTime.UtcNow, 100.0));
|
|
Assert.Equal(1, pubCount);
|
|
|
|
swma.Dispose();
|
|
|
|
publisher.Add(new TValue(DateTime.UtcNow.AddSeconds(1), 200.0));
|
|
Assert.Equal(1, pubCount); // Should not increment after dispose
|
|
}
|
|
|
|
// === Prime ===
|
|
|
|
[Fact]
|
|
public void Prime_SetsStateFromSpan()
|
|
{
|
|
var swma = new Swma(period: 4);
|
|
double[] data = { 10, 20, 30, 40, 50 };
|
|
swma.Prime(data);
|
|
|
|
Assert.True(swma.IsHot);
|
|
Assert.True(double.IsFinite(swma.Last.Value));
|
|
}
|
|
|
|
// === Triangular weight properties ===
|
|
|
|
[Fact]
|
|
public void Weights_AreSymmetric()
|
|
{
|
|
// Verify symmetry: output of mirror-reversed input equals original
|
|
var swma1 = new Swma(period: 5);
|
|
var swma2 = new Swma(period: 5);
|
|
double[] vals = { 10, 20, 30, 40, 50 };
|
|
double[] reversed = { 50, 40, 30, 20, 10 };
|
|
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
swma1.Update(new TValue(DateTime.UtcNow.AddSeconds(i), vals[i]));
|
|
swma2.Update(new TValue(DateTime.UtcNow.AddSeconds(i), reversed[i]));
|
|
}
|
|
|
|
// For symmetric filter with symmetric-around-center input:
|
|
// swma({10,20,30,40,50}) + swma({50,40,30,20,10}) should equal 2 * swma({30,30,30,30,30})
|
|
// Both outputs should be finite
|
|
Assert.True(double.IsFinite(swma1.Last.Value));
|
|
Assert.True(double.IsFinite(swma2.Last.Value));
|
|
// sum of outputs = 2 * center value (30) for symmetric weights
|
|
Assert.Equal(60.0, swma1.Last.Value + swma2.Last.Value, 1e-10);
|
|
}
|
|
|
|
[Fact]
|
|
public void Output_BoundedByInputRange()
|
|
{
|
|
// All weights non-negative: output is convex combination, bounded by min/max input
|
|
var swma = new Swma(period: 5);
|
|
double[] vals = { 10, 20, 30, 40, 50 };
|
|
for (int i = 0; i < vals.Length; i++)
|
|
{
|
|
swma.Update(new TValue(DateTime.UtcNow.AddSeconds(i), vals[i]));
|
|
}
|
|
|
|
Assert.InRange(swma.Last.Value, 10.0, 50.0);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_TSeries_EmptySource_ReturnsEmpty()
|
|
{
|
|
var swma = new Swma(period: 4);
|
|
var empty = new TSeries();
|
|
var result = swma.Update(empty);
|
|
Assert.Empty(result);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_TSeries_ProducesCorrectLength()
|
|
{
|
|
var src = MakeSeries(100);
|
|
var swma = new Swma(period: 4);
|
|
var result = swma.Update(src);
|
|
Assert.Equal(100, result.Count);
|
|
}
|
|
}
|