mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-16 01:28:05 +00:00
Refactor T3 Moving Average Implementation and Remove Unused Tests
- Deleted DebugTulip.Tests.cs as it was no longer needed. - Refactored T3.cs to encapsulate parameters in a struct for better organization and readability. - Updated methods in T3.cs to use the new Parameters struct, improving clarity and reducing redundancy. - Enhanced T3.md documentation to provide clearer explanations of the T3 moving average and its parameters. - Removed Wma.Coverage.Tests.cs as it was obsolete. - Added new tests in IndicatorExtensions.Tests.cs to validate logic methods and ensure correct calculations. - Updated IndicatorExtensions.cs to improve method organization and add new functionality for handling chart coordinates. - Refactored mocks in TradingPlatformMocks.cs to align with new chart interface definitions.
This commit is contained in:
@@ -1,87 +0,0 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class DemaCoverageTests
|
||||
{
|
||||
[Fact]
|
||||
public void Dema_CompensationLogic_IsTriggered()
|
||||
{
|
||||
// Compensation happens when E <= 1e-10
|
||||
// E starts at 1.0 and decays by (1-alpha) each step.
|
||||
// We need enough steps to reach 1e-10.
|
||||
// If period=10, alpha ~ 0.18, decay ~ 0.81
|
||||
// 0.81^n <= 1e-10 => n * log(0.81) <= -10
|
||||
// n * -0.09 <= -10 => n >= 111
|
||||
|
||||
int count = 200;
|
||||
int period = 10;
|
||||
var dema = new Dema(period);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
dema.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
}
|
||||
|
||||
// Just ensuring no exception and value is correct
|
||||
Assert.Equal(100.0, dema.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dema_IsHot_Logic()
|
||||
{
|
||||
// IsHot happens when E <= 0.05
|
||||
// 0.81^n <= 0.05 => n >= 14
|
||||
|
||||
int period = 10;
|
||||
var dema = new Dema(period);
|
||||
|
||||
Assert.False(dema.IsHot);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
dema.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
if (i > 30) // Should be hot by now
|
||||
{
|
||||
Assert.True(dema.IsHot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dema_StaticCalculate_Alpha_Coverage()
|
||||
{
|
||||
double[] source = new double[100];
|
||||
double[] output = new double[100];
|
||||
for(int i=0; i<100; i++) source[i] = 100.0;
|
||||
|
||||
// Use alpha overload
|
||||
Dema.Calculate(source.AsSpan(), output.AsSpan(), 0.1);
|
||||
|
||||
Assert.Equal(100.0, output[^1], 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dema_Reset_ClearsState()
|
||||
{
|
||||
var dema = new Dema(10);
|
||||
dema.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.NotEqual(0, dema.Last.Value);
|
||||
|
||||
dema.Reset();
|
||||
|
||||
Assert.Equal(0, dema.Last.Value);
|
||||
Assert.False(dema.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dema_Constructor_Alpha_Validation()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Dema(0.0));
|
||||
Assert.Throws<ArgumentException>(() => new Dema(1.1));
|
||||
|
||||
var dema = new Dema(0.5);
|
||||
Assert.NotNull(dema);
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class HmaCoverageTests
|
||||
{
|
||||
[Fact]
|
||||
public void Hma_CalculateIntermediate_Simd_Coverage()
|
||||
{
|
||||
// CalculateIntermediate uses SIMD if length >= Vector256<double>.Count (4)
|
||||
int count = 100;
|
||||
int period = 10;
|
||||
double[] source = new double[count];
|
||||
double[] output = new double[count];
|
||||
|
||||
for(int i=0; i<count; i++) source[i] = 100.0;
|
||||
|
||||
Hma.Calculate(source.AsSpan(), output.AsSpan(), period);
|
||||
|
||||
Assert.Equal(100.0, output[^1], 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hma_CalculateIntermediate_Scalar_Coverage()
|
||||
{
|
||||
// Force scalar path by using small length
|
||||
int count = 3;
|
||||
int period = 2; // Min period is 2
|
||||
double[] source = new double[count];
|
||||
double[] output = new double[count];
|
||||
|
||||
for(int i=0; i<count; i++) source[i] = 100.0;
|
||||
|
||||
Hma.Calculate(source.AsSpan(), output.AsSpan(), period);
|
||||
|
||||
Assert.Equal(100.0, output[^1], 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hma_Reset_ClearsState()
|
||||
{
|
||||
var hma = new Hma(10);
|
||||
hma.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.NotEqual(0, hma.Last.Value);
|
||||
|
||||
hma.Reset();
|
||||
|
||||
Assert.Equal(0, hma.Last.Value);
|
||||
Assert.False(hma.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hma_UpdateSeries_RestoresState()
|
||||
{
|
||||
var hma = new Hma(10);
|
||||
var series = new TSeries();
|
||||
for(int i=0; i<20; i++) series.Add(DateTime.UtcNow.AddMinutes(i), 100.0);
|
||||
|
||||
hma.Update(series);
|
||||
|
||||
// After batch update, the instance state should be consistent with the end of the series
|
||||
// So next update should continue correctly
|
||||
var nextVal = hma.Update(new TValue(DateTime.UtcNow.AddMinutes(20), 100.0));
|
||||
Assert.Equal(100.0, nextVal.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hma_Constructor_Validation()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Hma(1));
|
||||
Assert.Throws<ArgumentException>(() => new Hma(0));
|
||||
|
||||
var hma = new Hma(2);
|
||||
Assert.NotNull(hma);
|
||||
}
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class SmaCoverageTests
|
||||
{
|
||||
[Fact]
|
||||
public void Sma_ResyncLogic_IsTriggeredAndCorrect()
|
||||
{
|
||||
// ResyncInterval is 1000.
|
||||
int count = 2500;
|
||||
int period = 10;
|
||||
var sma = new Sma(period);
|
||||
|
||||
double constantValue = 100.0;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
sma.Update(new TValue(DateTime.UtcNow, constantValue));
|
||||
|
||||
if (i >= period)
|
||||
{
|
||||
Assert.Equal(constantValue, sma.Last.Value, 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sma_SpanCalc_LargeDataset_TriggersResync()
|
||||
{
|
||||
int count = 5000;
|
||||
int period = 10;
|
||||
double[] source = new double[count];
|
||||
double[] output = new double[count];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
source[i] = 100.0;
|
||||
}
|
||||
|
||||
Sma.Calculate(source.AsSpan(), output.AsSpan(), period);
|
||||
|
||||
for (int i = period; i < count; i++)
|
||||
{
|
||||
Assert.Equal(100.0, output[i], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sma_SpanCalc_SimdThreshold_Boundary()
|
||||
{
|
||||
// SimdThreshold is 256.
|
||||
int[] lengths = { 250, 256, 260 };
|
||||
int period = 10;
|
||||
|
||||
foreach (int len in lengths)
|
||||
{
|
||||
double[] source = new double[len];
|
||||
double[] output = new double[len];
|
||||
|
||||
for (int i = 0; i < len; i++) source[i] = 100.0;
|
||||
|
||||
Sma.Calculate(source.AsSpan(), output.AsSpan(), period);
|
||||
|
||||
Assert.Equal(100.0, output[^1], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sma_SpanCalc_Simd_WithResync()
|
||||
{
|
||||
int count = 3000;
|
||||
int period = 5;
|
||||
double[] source = new double[count];
|
||||
double[] output = new double[count];
|
||||
|
||||
// Linear increase: 0, 1, 2, ...
|
||||
for (int i = 0; i < count; i++) source[i] = i;
|
||||
|
||||
Sma.Calculate(source.AsSpan(), output.AsSpan(), period);
|
||||
|
||||
// SMA(5) of x-4, x-3, x-2, x-1, x
|
||||
// = (5x - 10) / 5 = x - 2
|
||||
|
||||
for (int i = period; i < count; i++)
|
||||
{
|
||||
double expected = i - 2.0;
|
||||
Assert.Equal(expected, output[i], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sma_Constructor_ThrowsOnInvalidPeriod()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Sma(0));
|
||||
Assert.Throws<ArgumentException>(() => new Sma(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sma_StaticCalculate_ThrowsOnInvalidArgs()
|
||||
{
|
||||
double[] source = new double[10];
|
||||
double[] output = new double[5]; // Mismatch
|
||||
Assert.Throws<ArgumentException>(() => Sma.Calculate(source.AsSpan(), output.AsSpan(), 5));
|
||||
|
||||
double[] output2 = new double[10];
|
||||
Assert.Throws<ArgumentException>(() => Sma.Calculate(source.AsSpan(), output2.AsSpan(), 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sma_Calculate_EmptyInput_DoesNothing()
|
||||
{
|
||||
Sma.Calculate(ReadOnlySpan<double>.Empty, Span<double>.Empty, 5);
|
||||
// Should not throw
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sma_Update_WithNaN_UsesLastValid()
|
||||
{
|
||||
var sma = new Sma(5);
|
||||
sma.Update(new TValue(DateTime.UtcNow, 1.0));
|
||||
sma.Update(new TValue(DateTime.UtcNow, 2.0));
|
||||
sma.Update(new TValue(DateTime.UtcNow, double.NaN)); // Should use 2.0
|
||||
|
||||
// Buffer: 1, 2, 2
|
||||
// SMA(3) = (1 + 2 + 2) / 3 = 5/3 = 1.666...
|
||||
|
||||
Assert.Equal(5.0/3.0, sma.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sma_Update_IsNewFalse_UpdatesLastValue()
|
||||
{
|
||||
var sma = new Sma(3);
|
||||
sma.Update(new TValue(DateTime.UtcNow, 1.0));
|
||||
sma.Update(new TValue(DateTime.UtcNow, 2.0));
|
||||
|
||||
// Update existing with 3.0 (replaces 2.0)
|
||||
sma.Update(new TValue(DateTime.UtcNow, 3.0), isNew: false);
|
||||
|
||||
// Buffer should be: 1, 3
|
||||
// SMA = (1 + 3) / 2 = 2
|
||||
|
||||
Assert.Equal(2.0, sma.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sma_TSeries_Empty_ReturnsEmpty()
|
||||
{
|
||||
var sma = new Sma(5);
|
||||
var result = sma.Update(new TSeries());
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sma_TSeries_WithNaN_RestoresStateCorrectly()
|
||||
{
|
||||
var sma = new Sma(3);
|
||||
var series = new TSeries();
|
||||
series.Add(new TValue(DateTime.UtcNow, 1.0));
|
||||
series.Add(new TValue(DateTime.UtcNow, 2.0));
|
||||
series.Add(new TValue(DateTime.UtcNow, double.NaN));
|
||||
series.Add(new TValue(DateTime.UtcNow, 4.0));
|
||||
|
||||
sma.Update(series);
|
||||
|
||||
// Buffer: 2.0, 2.0 (from NaN), 4.0
|
||||
// SMA(3) = (2 + 2 + 4) / 3 = 8/3 = 2.666...
|
||||
|
||||
// Let's add one more value to verify state is correct
|
||||
sma.Update(new TValue(DateTime.UtcNow, 5.0));
|
||||
|
||||
// Buffer: 2.0, 4.0, 5.0
|
||||
// SMA(3) = (2 + 4 + 5) / 3 = 11/3 = 3.666...
|
||||
|
||||
Assert.Equal(11.0/3.0, sma.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sma_Reset_ClearsState()
|
||||
{
|
||||
var sma = new Sma(3);
|
||||
sma.Update(new TValue(DateTime.UtcNow, 1.0));
|
||||
sma.Update(new TValue(DateTime.UtcNow, 2.0));
|
||||
sma.Update(new TValue(DateTime.UtcNow, 3.0));
|
||||
|
||||
sma.Reset();
|
||||
|
||||
Assert.Equal(0, sma.Last.Value);
|
||||
|
||||
// Start fresh
|
||||
sma.Update(new TValue(DateTime.UtcNow, 10.0));
|
||||
// Buffer: 10
|
||||
// SMA = 10
|
||||
Assert.Equal(10.0, sma.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sma_Calculate_ScalarFallback_WithNaN()
|
||||
{
|
||||
// Force scalar path by including NaN, even with large dataset
|
||||
int count = 1000;
|
||||
double[] source = new double[count];
|
||||
double[] output = new double[count];
|
||||
|
||||
for (int i = 0; i < count; i++) source[i] = 1.0;
|
||||
source[500] = double.NaN; // This should trigger HasNonFiniteValues -> true
|
||||
|
||||
Sma.Calculate(source.AsSpan(), output.AsSpan(), 10);
|
||||
|
||||
// Check around the NaN
|
||||
// Index 500 is NaN, so it uses previous valid (1.0)
|
||||
// So effectively the stream is all 1.0s
|
||||
Assert.Equal(1.0, output[500], 1e-9);
|
||||
Assert.Equal(1.0, output[501], 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sma_Constructor_WithSource_Subscribes()
|
||||
{
|
||||
var source = new Sma(10); // Just using Sma as a publisher
|
||||
var sma = new Sma(source, 5);
|
||||
|
||||
source.Update(new TValue(DateTime.UtcNow, 10.0));
|
||||
|
||||
Assert.Equal(10.0, sma.Last.Value);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Tulip;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class DebugTulipTests
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public DebugTulipTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ListTulipIndicators()
|
||||
{
|
||||
var type = typeof(Tulip.Indicators);
|
||||
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Static);
|
||||
var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static);
|
||||
|
||||
_output.WriteLine("Tulip Indicators (Properties):");
|
||||
foreach (var p in properties)
|
||||
{
|
||||
_output.WriteLine(p.Name);
|
||||
}
|
||||
|
||||
_output.WriteLine("Tulip Indicators (Fields):");
|
||||
foreach (var f in fields)
|
||||
{
|
||||
_output.WriteLine(f.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
-25
@@ -34,8 +34,22 @@ public sealed class T3 : ITValuePublisher
|
||||
public static State New() => new() { IsInitialized = false };
|
||||
}
|
||||
|
||||
private readonly double _alpha;
|
||||
private readonly double _c1, _c2, _c3, _c4;
|
||||
private readonly struct Parameters
|
||||
{
|
||||
public readonly double Alpha;
|
||||
public readonly double C1, C2, C3, C4;
|
||||
|
||||
public Parameters(double alpha, double c1, double c2, double c3, double c4)
|
||||
{
|
||||
Alpha = alpha;
|
||||
C1 = c1;
|
||||
C2 = c2;
|
||||
C3 = c3;
|
||||
C4 = c4;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Parameters _params;
|
||||
private State _state = State.New();
|
||||
private State _p_state = State.New();
|
||||
private double _lastValidValue;
|
||||
@@ -57,17 +71,19 @@ public sealed class T3 : ITValuePublisher
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
|
||||
_alpha = 2.0 / (period + 1);
|
||||
|
||||
double alpha = 2.0 / (period + 1);
|
||||
|
||||
// Precompute coefficients
|
||||
double v = vfactor;
|
||||
double v2 = v * v;
|
||||
double v3 = v2 * v;
|
||||
|
||||
_c1 = -v3;
|
||||
_c2 = 3.0 * (v2 + v3);
|
||||
_c3 = -3.0 * (2.0 * v2 + v + v3);
|
||||
_c4 = 1.0 + 3.0 * v + 3.0 * v2 + v3;
|
||||
double c1 = -v3;
|
||||
double c2 = 3.0 * (v2 + v3);
|
||||
double c3 = -3.0 * (2.0 * v2 + v + v3);
|
||||
double c4 = 1.0 + 3.0 * v + 3.0 * v2 + v3;
|
||||
|
||||
_params = new Parameters(alpha, c1, c2, c3, c4);
|
||||
|
||||
Name = $"T3({period}, {vfactor:F2})";
|
||||
}
|
||||
@@ -118,7 +134,7 @@ public sealed class T3 : ITValuePublisher
|
||||
}
|
||||
|
||||
double val = GetValidValue(input.Value);
|
||||
val = Compute(val, _alpha, _c1, _c2, _c3, _c4, ref _state);
|
||||
val = Compute(val, _params, ref _state);
|
||||
Last = new TValue(input.Time, val);
|
||||
Pub?.Invoke(Last);
|
||||
return Last;
|
||||
@@ -142,21 +158,21 @@ public sealed class T3 : ITValuePublisher
|
||||
State state = _state;
|
||||
double lastValidValue = _lastValidValue;
|
||||
|
||||
CalculateCore(sourceValues, vSpan, _alpha, _c1, _c2, _c3, _c4, ref state, ref lastValidValue);
|
||||
CalculateCore(sourceValues, vSpan, _params, ref state, ref lastValidValue);
|
||||
|
||||
_state = state;
|
||||
_lastValidValue = lastValidValue;
|
||||
|
||||
sourceTimes.CopyTo(tSpan);
|
||||
|
||||
|
||||
_p_state = _state;
|
||||
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
|
||||
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double Compute(double input, double alpha, double c1, double c2, double c3, double c4, ref State state)
|
||||
private static double Compute(double input, in Parameters p, ref State state)
|
||||
{
|
||||
if (!state.IsInitialized)
|
||||
{
|
||||
@@ -165,20 +181,19 @@ public sealed class T3 : ITValuePublisher
|
||||
}
|
||||
else
|
||||
{
|
||||
state.E1 += alpha * (input - state.E1);
|
||||
state.E2 += alpha * (state.E1 - state.E2);
|
||||
state.E3 += alpha * (state.E2 - state.E3);
|
||||
state.E4 += alpha * (state.E3 - state.E4);
|
||||
state.E5 += alpha * (state.E4 - state.E5);
|
||||
state.E6 += alpha * (state.E5 - state.E6);
|
||||
state.E1 += p.Alpha * (input - state.E1);
|
||||
state.E2 += p.Alpha * (state.E1 - state.E2);
|
||||
state.E3 += p.Alpha * (state.E2 - state.E3);
|
||||
state.E4 += p.Alpha * (state.E3 - state.E4);
|
||||
state.E5 += p.Alpha * (state.E4 - state.E5);
|
||||
state.E6 += p.Alpha * (state.E5 - state.E6);
|
||||
}
|
||||
|
||||
return c1 * state.E6 + c2 * state.E5 + c3 * state.E4 + c4 * state.E3;
|
||||
return p.C1 * state.E6 + p.C2 * state.E5 + p.C3 * state.E4 + p.C4 * state.E3;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void CalculateCore(ReadOnlySpan<double> source, Span<double> output, double alpha,
|
||||
double c1, double c2, double c3, double c4, ref State state, ref double lastValidValue)
|
||||
private static void CalculateCore(ReadOnlySpan<double> source, Span<double> output, in Parameters p, ref State state, ref double lastValidValue)
|
||||
{
|
||||
int len = source.Length;
|
||||
for (int i = 0; i < len; i++)
|
||||
@@ -189,7 +204,7 @@ public sealed class T3 : ITValuePublisher
|
||||
else
|
||||
val = lastValidValue;
|
||||
|
||||
output[i] = Compute(val, alpha, c1, c2, c3, c4, ref state);
|
||||
output[i] = Compute(val, p, ref state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +227,7 @@ public sealed class T3 : ITValuePublisher
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
if (source.Length != output.Length)
|
||||
throw new ArgumentException("Source and output must have the same length");
|
||||
|
||||
|
||||
double alpha = 2.0 / (period + 1);
|
||||
double v = vfactor;
|
||||
double v2 = v * v;
|
||||
@@ -223,10 +238,11 @@ public sealed class T3 : ITValuePublisher
|
||||
double c3 = -3.0 * (2.0 * v2 + v + v3);
|
||||
double c4 = 1.0 + 3.0 * v + 3.0 * v2 + v3;
|
||||
|
||||
var p = new Parameters(alpha, c1, c2, c3, c4);
|
||||
State state = State.New();
|
||||
double lastValidValue = 0;
|
||||
|
||||
CalculateCore(source, output, alpha, c1, c2, c3, c4, ref state, ref lastValidValue);
|
||||
CalculateCore(source, output, p, ref state, ref lastValidValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
# T3: Tillson T3 Moving Average
|
||||
|
||||
## Overview and Purpose
|
||||
|
||||
The Tillson T3 Moving Average is an advanced technical indicator designed to provide superior smoothing with minimal lag. Developed by Tim Tillson and introduced in the January 1998 issue of Technical Analysis of Stocks & Commodities magazine, T3 implements a sophisticated six-stage EMA architecture with optimized coefficient distribution based on a volume factor parameter.
|
||||
|
||||
Unlike simpler moving averages or even triple-EMA approaches, T3 uses a unique mathematical framework that strategically combines multiple EMAs with precisely calculated coefficients. This approach creates a moving average that effectively reduces noise while preserving important trend information and minimizing lag.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
* **Multi-stage smoothing:** Uses a six-stage EMA cascade with optimized coefficient distribution to achieve superior noise reduction while minimizing lag
|
||||
* **Volume factor customization:** Provides a parameter that allows traders to fine-tune the balance between smoothness and responsiveness
|
||||
* **Strategic coefficient weighting:** Employs a sophisticated formula that prevents overshooting at turning points while maintaining responsiveness
|
||||
|
||||
## Calculation and Mathematical Foundation
|
||||
|
||||
T3 works by running price data through a series of six EMAs, then combining the outputs of these EMAs using carefully calculated weights. These weights are determined by a "volume factor" parameter ($v$) that controls how much the indicator prioritizes smoothness versus responsiveness.
|
||||
|
||||
### Formula
|
||||
|
||||
$$ T3 = c_1 \cdot EMA_6 + c_2 \cdot EMA_5 + c_3 \cdot EMA_4 + c_4 \cdot EMA_3 $$
|
||||
|
||||
Where:
|
||||
|
||||
* $EMA_1$ through $EMA_6$ are exponential moving averages applied in sequence:
|
||||
* $EMA_1(x) = EMA(x)$
|
||||
* $EMA_n(x) = EMA(EMA_{n-1}(x))$
|
||||
@@ -28,6 +33,7 @@ Where:
|
||||
* Default volume factor $v = 0.7$
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Default | Range | Description |
|
||||
|-----------|---------|-------|-------------|
|
||||
| Period | 10 | > 0 | The smoothing period for the internal EMAs |
|
||||
@@ -36,6 +42,7 @@ Where:
|
||||
## C# Usage
|
||||
|
||||
### Standard TSeries Usage
|
||||
|
||||
```csharp
|
||||
// Calculate T3 with period 10 and default volume factor 0.7
|
||||
var t3 = T3.Calculate(sourceSeries, 10);
|
||||
@@ -47,6 +54,7 @@ Console.WriteLine($"T3 Value: {t3.Last.Value}");
|
||||
```
|
||||
|
||||
### Eventing and Reactive Support
|
||||
|
||||
The `T3` class implements `ITValuePublisher`, allowing for event-driven updates.
|
||||
|
||||
```csharp
|
||||
|
||||
@@ -1,276 +0,0 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class WmaCoverageTests
|
||||
{
|
||||
[Fact]
|
||||
public void Wma_ResyncLogic_IsTriggeredAndCorrect()
|
||||
{
|
||||
// ResyncInterval is 1000. We need more than that to trigger it.
|
||||
int count = 2500;
|
||||
int period = 10;
|
||||
var wma = new Wma(period);
|
||||
|
||||
// Use a constant value to make verification easy
|
||||
// WMA of constant X is X
|
||||
double constantValue = 100.0;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
wma.Update(new TValue(DateTime.UtcNow, constantValue));
|
||||
|
||||
// After warmup, value should always be constantValue
|
||||
if (i >= period)
|
||||
{
|
||||
Assert.Equal(constantValue, wma.Last.Value, 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_SpanCalc_LargeDataset_TriggersResync()
|
||||
{
|
||||
// ResyncInterval is 1000.
|
||||
int count = 5000;
|
||||
int period = 10;
|
||||
double[] source = new double[count];
|
||||
double[] output = new double[count];
|
||||
|
||||
// Fill with constant value
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
source[i] = 100.0;
|
||||
}
|
||||
|
||||
Wma.Calculate(source.AsSpan(), output.AsSpan(), period);
|
||||
|
||||
// Verify all outputs after warmup are correct
|
||||
for (int i = period; i < count; i++)
|
||||
{
|
||||
Assert.Equal(100.0, output[i], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_SpanCalc_SimdThreshold_Boundary()
|
||||
{
|
||||
// SimdThreshold is 256.
|
||||
// Test just below and just above to ensure both paths work
|
||||
int[] lengths = { 250, 256, 260 };
|
||||
int period = 10;
|
||||
|
||||
foreach (int len in lengths)
|
||||
{
|
||||
double[] source = new double[len];
|
||||
double[] output = new double[len];
|
||||
|
||||
for (int i = 0; i < len; i++) source[i] = 100.0;
|
||||
|
||||
Wma.Calculate(source.AsSpan(), output.AsSpan(), period);
|
||||
|
||||
Assert.Equal(100.0, output[^1], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_SpanCalc_Simd_WithResync()
|
||||
{
|
||||
// This targets the SIMD loop with resync
|
||||
// Need length > SimdThreshold (256) and enough data to hit ResyncInterval (1000)
|
||||
// But wait, the SIMD loop in CalculateSimdCore handles resync internally.
|
||||
// The loop structure is:
|
||||
// while (idx < simdEnd)
|
||||
// nextSync = Math.Min(simdEnd, idx + ResyncInterval)
|
||||
// ... process blocks ...
|
||||
|
||||
int count = 3000;
|
||||
int period = 5;
|
||||
double[] source = new double[count];
|
||||
double[] output = new double[count];
|
||||
|
||||
// Use a pattern that isn't constant to verify calculation accuracy
|
||||
// Linear increase: 0, 1, 2, ...
|
||||
for (int i = 0; i < count; i++) source[i] = i;
|
||||
|
||||
Wma.Calculate(source.AsSpan(), output.AsSpan(), period);
|
||||
|
||||
// Verify a few points
|
||||
// WMA(5) of x-4, x-3, x-2, x-1, x
|
||||
// = (1*(x-4) + 2*(x-3) + 3*(x-2) + 4*(x-1) + 5*x) / 15
|
||||
// = (x-4 + 2x-6 + 3x-6 + 4x-4 + 5x) / 15
|
||||
// = (15x - 20) / 15
|
||||
// = x - 20/15 = x - 1.333...
|
||||
|
||||
for (int i = period; i < count; i++)
|
||||
{
|
||||
double expected = i - (20.0 / 15.0);
|
||||
Assert.Equal(expected, output[i], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_Update_Resync_WithFloatingPointDrift()
|
||||
{
|
||||
// This test tries to accumulate error and see if resync fixes it (or at least doesn't break it)
|
||||
// It's hard to deterministically cause drift, but we can ensure the code path is executed.
|
||||
int period = 10;
|
||||
var wma = new Wma(period);
|
||||
|
||||
// 1200 updates to trigger resync (at 1000)
|
||||
for (int i = 0; i < 1200; i++)
|
||||
{
|
||||
wma.Update(new TValue(DateTime.UtcNow, 1.0));
|
||||
}
|
||||
|
||||
Assert.Equal(1.0, wma.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_Constructor_ThrowsOnInvalidPeriod()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Wma(0));
|
||||
Assert.Throws<ArgumentException>(() => new Wma(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_StaticCalculate_ThrowsOnInvalidArgs()
|
||||
{
|
||||
double[] source = new double[10];
|
||||
double[] output = new double[5]; // Mismatch
|
||||
Assert.Throws<ArgumentException>(() => Wma.Calculate(source.AsSpan(), output.AsSpan(), 5));
|
||||
|
||||
double[] output2 = new double[10];
|
||||
Assert.Throws<ArgumentException>(() => Wma.Calculate(source.AsSpan(), output2.AsSpan(), 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_Calculate_EmptyInput_DoesNothing()
|
||||
{
|
||||
Wma.Calculate(ReadOnlySpan<double>.Empty, Span<double>.Empty, 5);
|
||||
// Should not throw
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_Update_WithNaN_UsesLastValid()
|
||||
{
|
||||
var wma = new Wma(5);
|
||||
wma.Update(new TValue(DateTime.UtcNow, 1.0));
|
||||
wma.Update(new TValue(DateTime.UtcNow, 2.0));
|
||||
wma.Update(new TValue(DateTime.UtcNow, double.NaN)); // Should use 2.0
|
||||
|
||||
// Buffer: 1, 2, 2
|
||||
// WMA(3) = (1*1 + 2*2 + 3*2) / 6 = (1 + 4 + 6) / 6 = 11/6 = 1.8333...
|
||||
// Wait, period is 5.
|
||||
// Buffer: 1, 2, 2
|
||||
// Sum = 5, WSum = 1*1 + 2*2 + 3*2 = 11
|
||||
// Divisor = 3*4/2 = 6
|
||||
// Result = 11/6
|
||||
|
||||
Assert.Equal(11.0/6.0, wma.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_Update_IsNewFalse_UpdatesLastValue()
|
||||
{
|
||||
var wma = new Wma(3);
|
||||
wma.Update(new TValue(DateTime.UtcNow, 1.0));
|
||||
wma.Update(new TValue(DateTime.UtcNow, 2.0));
|
||||
|
||||
// Update existing with 3.0 (replaces 2.0)
|
||||
wma.Update(new TValue(DateTime.UtcNow, 3.0), isNew: false);
|
||||
|
||||
// Buffer should be: 1, 3
|
||||
// Sum = 4, WSum = 1*1 + 2*3 = 7
|
||||
// Divisor = 2*3/2 = 3
|
||||
// Result = 7/3 = 2.333...
|
||||
|
||||
Assert.Equal(7.0/3.0, wma.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_TSeries_Empty_ReturnsEmpty()
|
||||
{
|
||||
var wma = new Wma(5);
|
||||
var result = wma.Update(new TSeries());
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_TSeries_WithNaN_RestoresStateCorrectly()
|
||||
{
|
||||
// This tests the state restoration logic in Update(TSeries)
|
||||
// specifically the loop that looks for _lastValidValue
|
||||
var wma = new Wma(3);
|
||||
var series = new TSeries();
|
||||
series.Add(new TValue(DateTime.UtcNow, 1.0));
|
||||
series.Add(new TValue(DateTime.UtcNow, 2.0));
|
||||
series.Add(new TValue(DateTime.UtcNow, double.NaN));
|
||||
series.Add(new TValue(DateTime.UtcNow, 4.0));
|
||||
|
||||
wma.Update(series);
|
||||
|
||||
// After processing series, internal state should match having processed these sequentially
|
||||
// Last value was 4.0. Previous valid was 2.0 (since NaN used 2.0).
|
||||
// Buffer: 2.0, 2.0 (from NaN), 4.0
|
||||
|
||||
// Let's add one more value to verify state is correct
|
||||
wma.Update(new TValue(DateTime.UtcNow, 5.0));
|
||||
|
||||
// Buffer: 2.0, 4.0, 5.0
|
||||
// WMA(3) = (1*2 + 2*4 + 3*5) / 6 = (2 + 8 + 15) / 6 = 25/6 = 4.1666...
|
||||
|
||||
Assert.Equal(25.0/6.0, wma.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_Reset_ClearsState()
|
||||
{
|
||||
var wma = new Wma(3);
|
||||
wma.Update(new TValue(DateTime.UtcNow, 1.0));
|
||||
wma.Update(new TValue(DateTime.UtcNow, 2.0));
|
||||
wma.Update(new TValue(DateTime.UtcNow, 3.0));
|
||||
|
||||
wma.Reset();
|
||||
|
||||
Assert.Equal(0, wma.Last.Value);
|
||||
|
||||
// Start fresh
|
||||
wma.Update(new TValue(DateTime.UtcNow, 10.0));
|
||||
// Buffer: 10
|
||||
// WMA = 10
|
||||
Assert.Equal(10.0, wma.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_Calculate_ScalarFallback_WithNaN()
|
||||
{
|
||||
// Force scalar path by including NaN, even with large dataset
|
||||
int count = 1000;
|
||||
double[] source = new double[count];
|
||||
double[] output = new double[count];
|
||||
|
||||
for (int i = 0; i < count; i++) source[i] = 1.0;
|
||||
source[500] = double.NaN; // This should trigger HasNonFiniteValues -> true
|
||||
|
||||
Wma.Calculate(source.AsSpan(), output.AsSpan(), 10);
|
||||
|
||||
// Check around the NaN
|
||||
// Index 500 is NaN, so it uses previous valid (1.0)
|
||||
// So effectively the stream is all 1.0s
|
||||
Assert.Equal(1.0, output[500], 1e-9);
|
||||
Assert.Equal(1.0, output[501], 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_Constructor_WithSource_Subscribes()
|
||||
{
|
||||
var source = new Wma(10); // Just using Wma as a publisher
|
||||
var wma = new Wma(source, 5);
|
||||
|
||||
source.Update(new TValue(DateTime.UtcNow, 10.0));
|
||||
|
||||
Assert.Equal(10.0, wma.Last.Value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user