mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-13 08:08:05 +00:00
- 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
465 lines
13 KiB
C#
465 lines
13 KiB
C#
namespace QuanTAlib.Tests;
|
|
|
|
public class Tukey_wTests
|
|
{
|
|
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_Is20()
|
|
{
|
|
var tw = new Tukey_w();
|
|
Assert.Equal("Tukey_w(20,0.50)", tw.Name);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_CustomPeriodAndAlpha()
|
|
{
|
|
var tw = new Tukey_w(period: 10, alpha: 0.3);
|
|
Assert.Equal("Tukey_w(10,0.30)", tw.Name);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_Period2_IsValid()
|
|
{
|
|
var tw = new Tukey_w(period: 2);
|
|
Assert.Equal("Tukey_w(2,0.50)", tw.Name);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_PeriodBelow2_Throws()
|
|
{
|
|
var ex = Assert.Throws<ArgumentException>(() => new Tukey_w(period: 1));
|
|
Assert.Equal("period", ex.ParamName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_NegativePeriod_Throws()
|
|
{
|
|
var ex = Assert.Throws<ArgumentException>(() => new Tukey_w(period: -5));
|
|
Assert.Equal("period", ex.ParamName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_AlphaBelowZero_Throws()
|
|
{
|
|
var ex = Assert.Throws<ArgumentException>(() => new Tukey_w(period: 10, alpha: -0.1));
|
|
Assert.Equal("alpha", ex.ParamName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_AlphaAboveOne_Throws()
|
|
{
|
|
var ex = Assert.Throws<ArgumentException>(() => new Tukey_w(period: 10, alpha: 1.1));
|
|
Assert.Equal("alpha", ex.ParamName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_AlphaZero_IsValid()
|
|
{
|
|
var tw = new Tukey_w(period: 5, alpha: 0.0);
|
|
Assert.Contains("0.00", tw.Name, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_AlphaOne_IsValid()
|
|
{
|
|
var tw = new Tukey_w(period: 5, alpha: 1.0);
|
|
Assert.Contains("1.00", tw.Name, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_SetsWarmupPeriod()
|
|
{
|
|
var tw = new Tukey_w(period: 8);
|
|
Assert.Equal(8, tw.WarmupPeriod);
|
|
}
|
|
|
|
// === B) Basic calculation ===
|
|
|
|
[Fact]
|
|
public void Update_ReturnsTValue()
|
|
{
|
|
var tw = new Tukey_w(period: 4);
|
|
var result = tw.Update(new TValue(DateTime.UtcNow, 100.0));
|
|
Assert.True(double.IsFinite(result.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_ConstantInput_ReturnsConstant()
|
|
{
|
|
var tw = new Tukey_w(period: 5, alpha: 0.5);
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
tw.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 42.0));
|
|
}
|
|
Assert.Equal(42.0, tw.Last.Value, 1e-10);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_Alpha0_EquivalentToSMA()
|
|
{
|
|
// alpha=0 → rectangular window → SMA
|
|
int period = 5;
|
|
var tw = new Tukey_w(period: period, alpha: 0.0);
|
|
double[] vals = { 10, 20, 30, 40, 50 };
|
|
for (int i = 0; i < vals.Length; i++)
|
|
{
|
|
tw.Update(new TValue(DateTime.UtcNow.AddSeconds(i), vals[i]));
|
|
}
|
|
// SMA(5) = (10+20+30+40+50)/5 = 30
|
|
Assert.Equal(30.0, tw.Last.Value, 1e-10);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_Alpha0_LargerSeries_MatchesSMA()
|
|
{
|
|
var src = MakeSeries(100);
|
|
int period = 10;
|
|
|
|
var tw = Tukey_w.Batch(src, period, alpha: 0.0);
|
|
var sma = Sma.Batch(src, period);
|
|
|
|
// After warmup, should match SMA exactly
|
|
for (int i = period - 1; i < src.Count; i++)
|
|
{
|
|
Assert.Equal(sma[i].Value, tw[i].Value, 1e-10);
|
|
}
|
|
}
|
|
|
|
// === C) State + bar correction ===
|
|
|
|
[Fact]
|
|
public void Update_IsNew_True_AdvancesState()
|
|
{
|
|
var tw = new Tukey_w(period: 4);
|
|
tw.Update(new TValue(DateTime.UtcNow, 100.0), isNew: true);
|
|
tw.Update(new TValue(DateTime.UtcNow.AddSeconds(1), 110.0), isNew: true);
|
|
var r1 = tw.Last;
|
|
tw.Update(new TValue(DateTime.UtcNow.AddSeconds(2), 120.0), isNew: true);
|
|
Assert.NotEqual(r1.Value, tw.Last.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_IsNew_False_Rewrites()
|
|
{
|
|
var tw = new Tukey_w(period: 4, alpha: 0.5);
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
tw.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i), isNew: true);
|
|
}
|
|
var afterNew = tw.Last;
|
|
|
|
tw.Update(new TValue(DateTime.UtcNow.AddSeconds(4), 104.0), isNew: false);
|
|
Assert.Equal(afterNew.Value, tw.Last.Value, 1e-10);
|
|
}
|
|
|
|
[Fact]
|
|
public void Reset_ClearsState()
|
|
{
|
|
var tw = new Tukey_w(period: 4);
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
tw.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i));
|
|
}
|
|
Assert.True(tw.IsHot);
|
|
|
|
tw.Reset();
|
|
Assert.False(tw.IsHot);
|
|
Assert.Equal(default, tw.Last);
|
|
}
|
|
|
|
// === D) Warmup/convergence ===
|
|
|
|
[Fact]
|
|
public void IsHot_FlipsAtPeriod()
|
|
{
|
|
var tw = new Tukey_w(period: 5);
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
tw.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i));
|
|
Assert.False(tw.IsHot);
|
|
}
|
|
tw.Update(new TValue(DateTime.UtcNow.AddSeconds(4), 104.0));
|
|
Assert.True(tw.IsHot);
|
|
}
|
|
|
|
[Fact]
|
|
public void DuringWarmup_ReturnsRawValue()
|
|
{
|
|
var tw = new Tukey_w(period: 5);
|
|
var result = tw.Update(new TValue(DateTime.UtcNow, 42.0));
|
|
Assert.Equal(42.0, result.Value, 1e-10);
|
|
}
|
|
|
|
// === E) Robustness ===
|
|
|
|
[Fact]
|
|
public void Update_NaN_UsesLastValid()
|
|
{
|
|
var tw = new Tukey_w(period: 4);
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
tw.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0));
|
|
}
|
|
|
|
tw.Update(new TValue(DateTime.UtcNow.AddSeconds(5), double.NaN));
|
|
Assert.True(double.IsFinite(tw.Last.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_Infinity_UsesLastValid()
|
|
{
|
|
var tw = new Tukey_w(period: 4);
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
tw.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0));
|
|
}
|
|
|
|
tw.Update(new TValue(DateTime.UtcNow.AddSeconds(5), double.PositiveInfinity));
|
|
Assert.True(double.IsFinite(tw.Last.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_FirstValueNaN_ReturnsNaN()
|
|
{
|
|
var tw = new Tukey_w(period: 4);
|
|
var result = tw.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];
|
|
Tukey_w.Batch(source, output, period: 3, alpha: 0.5);
|
|
|
|
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;
|
|
double alpha = 0.4;
|
|
|
|
// Mode 1: Streaming
|
|
var streaming = new Tukey_w(period, alpha);
|
|
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 = Tukey_w.Batch(src, period, alpha);
|
|
|
|
// Mode 3: Span API
|
|
var spanOutput = new double[src.Count];
|
|
Tukey_w.Batch(src.Values, spanOutput, period, alpha);
|
|
|
|
// Mode 4: Event-based
|
|
var publisher = new TSeries();
|
|
var eventResults = new List<double>();
|
|
var eventTw = new Tukey_w(publisher, period, alpha);
|
|
eventTw.Pub += (object? sender, in TValueEventArgs e) => eventResults.Add(e.Value.Value);
|
|
for (int i = 0; i < src.Count; i++)
|
|
{
|
|
publisher.Add(src[i]);
|
|
}
|
|
|
|
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));
|
|
Assert.True(double.IsNaN(sp));
|
|
Assert.True(double.IsNaN(ev));
|
|
}
|
|
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>(() => Tukey_w.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>(() => Tukey_w.Batch(source, output, period: 1));
|
|
Assert.Equal("period", ex.ParamName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Batch_Span_AlphaOutOfRange_Throws()
|
|
{
|
|
double[] source = { 1, 2, 3 };
|
|
double[] output = new double[3];
|
|
|
|
Assert.Throws<ArgumentException>(() => Tukey_w.Batch(source, output, period: 2, alpha: -0.1));
|
|
Assert.Throws<ArgumentException>(() => Tukey_w.Batch(source, output, period: 2, alpha: 1.5));
|
|
}
|
|
|
|
[Fact]
|
|
public void Batch_Span_EmptyInput_NoOutput()
|
|
{
|
|
Tukey_w.Batch(ReadOnlySpan<double>.Empty, Span<double>.Empty, period: 4);
|
|
Assert.True(true);
|
|
}
|
|
|
|
[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);
|
|
}
|
|
|
|
Tukey_w.Batch(source, output, period: 20, alpha: 0.5);
|
|
Assert.True(double.IsFinite(output[^1]));
|
|
}
|
|
|
|
// === H) Chainability ===
|
|
|
|
[Fact]
|
|
public void Pub_FiresOnUpdate()
|
|
{
|
|
var tw = new Tukey_w(period: 4);
|
|
int pubCount = 0;
|
|
tw.Pub += (object? sender, in TValueEventArgs e) => pubCount++;
|
|
|
|
tw.Update(new TValue(DateTime.UtcNow, 100.0));
|
|
Assert.Equal(1, pubCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void EventBased_Chaining_Works()
|
|
{
|
|
var publisher = new TSeries();
|
|
var tw = new Tukey_w(publisher, period: 4, alpha: 0.5);
|
|
int resultCount = 0;
|
|
tw.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 ===
|
|
|
|
[Fact]
|
|
public void Calculate_ReturnsResultsAndIndicator()
|
|
{
|
|
var src = MakeSeries(50);
|
|
var (results, indicator) = Tukey_w.Calculate(src, period: 5, alpha: 0.5);
|
|
|
|
Assert.Equal(50, results.Count);
|
|
Assert.True(indicator.IsHot);
|
|
}
|
|
|
|
[Fact]
|
|
public void Dispose_UnsubscribesFromSource()
|
|
{
|
|
var publisher = new TSeries();
|
|
var tw = new Tukey_w(publisher, period: 4);
|
|
int pubCount = 0;
|
|
tw.Pub += (object? sender, in TValueEventArgs e) => pubCount++;
|
|
|
|
publisher.Add(new TValue(DateTime.UtcNow, 100.0));
|
|
Assert.Equal(1, pubCount);
|
|
|
|
tw.Dispose();
|
|
|
|
publisher.Add(new TValue(DateTime.UtcNow.AddSeconds(1), 200.0));
|
|
Assert.Equal(1, pubCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void Prime_SetsStateFromSpan()
|
|
{
|
|
var tw = new Tukey_w(period: 4);
|
|
double[] data = { 10, 20, 30, 40, 50 };
|
|
tw.Prime(data);
|
|
|
|
Assert.True(tw.IsHot);
|
|
Assert.True(double.IsFinite(tw.Last.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Output_BoundedByInputRange()
|
|
{
|
|
// All weights non-negative: output bounded by min/max input
|
|
var tw = new Tukey_w(period: 5, alpha: 0.5);
|
|
double[] vals = { 10, 20, 30, 40, 50 };
|
|
for (int i = 0; i < vals.Length; i++)
|
|
{
|
|
tw.Update(new TValue(DateTime.UtcNow.AddSeconds(i), vals[i]));
|
|
}
|
|
Assert.InRange(tw.Last.Value, 10.0, 50.0);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_TSeries_EmptySource_ReturnsEmpty()
|
|
{
|
|
var tw = new Tukey_w(period: 4);
|
|
var result = tw.Update(new TSeries());
|
|
Assert.Empty(result);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_TSeries_ProducesCorrectLength()
|
|
{
|
|
var src = MakeSeries(100);
|
|
var tw = new Tukey_w(period: 5);
|
|
var result = tw.Update(src);
|
|
Assert.Equal(100, result.Count);
|
|
}
|
|
}
|