mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-02 03:37:42 +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);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using Xunit;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using TradingPlatform.BusinessLayer.Chart;
|
||||
using System.Drawing;
|
||||
using System.Reflection;
|
||||
|
||||
@@ -15,14 +16,14 @@ public class IndicatorExtensionsTests
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestCoordinatesConverter : ICoordinatesConverter
|
||||
private sealed class TestCoordinatesConverter : IChartWindowCoordinatesConverter
|
||||
{
|
||||
private readonly DateTime _time;
|
||||
public TestCoordinatesConverter(DateTime time) => _time = time;
|
||||
|
||||
public DateTime GetTime(int x) => _time;
|
||||
public double GetChartX(DateTime time) => 0;
|
||||
public double GetChartY(double value) => 0;
|
||||
public double GetChartX(DateTime time) => 10; // Return a fixed X for testing
|
||||
public double GetChartY(double value) => value; // Return value as Y for testing
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -108,21 +109,8 @@ public class IndicatorExtensionsTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
|
||||
public void PaintMethods_DoNotThrow_WithValidGraphics()
|
||||
public void LogicMethods_CalculateCorrectly()
|
||||
{
|
||||
// This test attempts to verify that paint methods don't crash.
|
||||
// It requires System.Drawing.Common to be functional.
|
||||
|
||||
if (!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
|
||||
{
|
||||
// Skip on non-Windows if System.Drawing is not fully supported (GDI+)
|
||||
return;
|
||||
}
|
||||
|
||||
using var bitmap = new Bitmap(100, 100);
|
||||
using var graphics = Graphics.FromImage(bitmap);
|
||||
|
||||
var indicator = new TestIndicator();
|
||||
indicator.CurrentChart = new MockChart();
|
||||
|
||||
@@ -133,9 +121,92 @@ public class IndicatorExtensionsTests
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 110, 90, 105);
|
||||
}
|
||||
|
||||
// Setup converter to return a time that exists in our data (e.g. the middle bar)
|
||||
// We added bars at now, now+1min, ..., now+19min.
|
||||
// Let's return now+10min.
|
||||
// Setup converter
|
||||
var validTime = now.AddMinutes(10);
|
||||
var converter = new TestCoordinatesConverter(validTime);
|
||||
indicator.CurrentChart.MainWindow.CoordinatesConverter = converter;
|
||||
|
||||
var clientRect = new Rectangle(0, 0, 100, 100);
|
||||
|
||||
// 1. Test GetHLineY
|
||||
int y = IndicatorExtensions.GetHLineY(converter, 50.0);
|
||||
Assert.Equal(50, y); // Since our mock returns value as Y
|
||||
|
||||
// 2. Test GetSmoothCurvePoints
|
||||
var series = new LineSeries("Test", Color.Blue, 1, LineStyle.Solid);
|
||||
for (int i = 0; i < 20; i++) series.AddValue();
|
||||
for (int i = 0; i < 20; i++) series.SetValue(100 + i, i);
|
||||
|
||||
var points = IndicatorExtensions.GetSmoothCurvePoints(indicator, converter, clientRect, series);
|
||||
Assert.NotEmpty(points);
|
||||
// Verify points logic: X should be 10 + halfBarWidth, Y should be value
|
||||
// MockChart.BarsWidth defaults to something? Let's assume 0 or check logic.
|
||||
// In GetSmoothCurvePoints: barX + halfBarWidth.
|
||||
// Our mock GetChartX returns 10.
|
||||
|
||||
// 3. Test GetHistogramRectangles
|
||||
var histSeries = new LineSeries("Hist", Color.Blue, 1, LineStyle.Solid);
|
||||
for (int i = 0; i < 20; i++) histSeries.AddValue();
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double val = (i % 2 == 0) ? 10.0 : -10.0;
|
||||
histSeries.SetValue(val, i);
|
||||
}
|
||||
|
||||
var rects = IndicatorExtensions.GetHistogramRectangles(indicator, converter, clientRect, histSeries);
|
||||
Assert.NotEmpty(rects);
|
||||
|
||||
// Check value at offset 9 (i=9 in setup loop)
|
||||
// i=9 is odd -> -10.0 (Negative)
|
||||
// Color should be Red (150, 255, 0, 0)
|
||||
var first = rects.First();
|
||||
Assert.Equal(Color.FromArgb(150, 255, 0, 0), first.Color);
|
||||
|
||||
// Verify geometry
|
||||
// Value is -10. GetChartY(-10) -> -10.
|
||||
// GetChartY(0) -> 0.
|
||||
// Height = Abs(0 - (-10)) = 10.
|
||||
// Y = 0 (since negative bars start at 0 and go down? No, GDI+ coords usually go down.
|
||||
// But here we are testing the logic in GetHistogramRectangles:
|
||||
// else { new Rectangle(barX, barY0, ...) } -> Y = barY0 = 0.
|
||||
Assert.Equal(0, first.Rect.Y);
|
||||
Assert.Equal(10, first.Rect.Height);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
|
||||
public void PaintMethods_DoNotThrow_WithValidGraphics()
|
||||
{
|
||||
// This test attempts to verify that paint methods don't crash.
|
||||
// It requires System.Drawing.Common to be functional.
|
||||
|
||||
// On non-Windows, this might fail if libgdiplus is not installed.
|
||||
// We'll try-catch the PlatformNotSupportedException to allow the test to pass (but not cover) on those systems.
|
||||
try
|
||||
{
|
||||
using var bitmap = new Bitmap(100, 100);
|
||||
using var graphics = Graphics.FromImage(bitmap);
|
||||
RunPaintTests(graphics);
|
||||
}
|
||||
catch (TypeInitializationException) { return; } // System.Drawing.Common not supported
|
||||
catch (PlatformNotSupportedException) { return; } // GDI+ not available
|
||||
catch (DllNotFoundException) { return; } // libgdiplus not found
|
||||
}
|
||||
|
||||
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
|
||||
private void RunPaintTests(Graphics graphics)
|
||||
{
|
||||
var indicator = new TestIndicator();
|
||||
indicator.CurrentChart = new MockChart();
|
||||
|
||||
// Add some data
|
||||
var now = new DateTime(2024, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 110, 90, 105);
|
||||
}
|
||||
|
||||
// Setup converter
|
||||
var validTime = now.AddMinutes(10);
|
||||
indicator.CurrentChart.MainWindow.CoordinatesConverter = new TestCoordinatesConverter(validTime);
|
||||
|
||||
@@ -145,20 +216,49 @@ public class IndicatorExtensionsTests
|
||||
// Test PaintHLine
|
||||
IndicatorExtensions.PaintHLine(indicator, args, 100, pen);
|
||||
|
||||
// Test PaintSmoothCurve
|
||||
var series = new LineSeries("Test", Color.Blue, 1, LineStyle.Solid);
|
||||
for (int i = 0; i < 20; i++) series.AddValue(); // Fill with NaNs or values
|
||||
for (int i = 0; i < 20; i++) series.SetValue(100 + i, i); // Set some values
|
||||
|
||||
IndicatorExtensions.PaintSmoothCurve(indicator, args, series, 0);
|
||||
// Test PaintSmoothCurve with different LineStyles and Warmup
|
||||
foreach (LineStyle style in Enum.GetValues(typeof(LineStyle)))
|
||||
{
|
||||
var series = new LineSeries("Test", Color.Blue, 1, style);
|
||||
for (int i = 0; i < 20; i++) series.AddValue();
|
||||
for (int i = 0; i < 20; i++) series.SetValue(100 + i, i);
|
||||
|
||||
// Test with warmup and cold values
|
||||
IndicatorExtensions.PaintSmoothCurve(indicator, args, series, warmupPeriod: 5, showColdValues: true);
|
||||
|
||||
// Test without cold values
|
||||
IndicatorExtensions.PaintSmoothCurve(indicator, args, series, warmupPeriod: 5, showColdValues: false);
|
||||
}
|
||||
|
||||
// Test PaintHistogram
|
||||
IndicatorExtensions.PaintHistogram(indicator, args, series, 0);
|
||||
// Test PaintHistogram with Positive and Negative values
|
||||
var histSeries = new LineSeries("Hist", Color.Blue, 1, LineStyle.Solid);
|
||||
for (int i = 0; i < 20; i++) histSeries.AddValue();
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
// Alternate positive and negative
|
||||
double val = (i % 2 == 0) ? 10.0 : -10.0;
|
||||
histSeries.SetValue(val, i);
|
||||
}
|
||||
IndicatorExtensions.PaintHistogram(indicator, args, histSeries, 0);
|
||||
|
||||
// Test DrawText
|
||||
IndicatorExtensions.DrawText(indicator, args, "Test Text");
|
||||
|
||||
// Assert that we reached the end without throwing
|
||||
// If we got here, no exception was thrown
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetInputValue_DefaultCase_ReturnsClose()
|
||||
{
|
||||
TestIndicator indicator = new();
|
||||
DateTime now = new(2024, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105, 1000);
|
||||
UpdateArgs args = new(UpdateReason.NewBar);
|
||||
|
||||
// Cast to an invalid SourceType to trigger default case
|
||||
SourceType invalidType = (SourceType)999;
|
||||
|
||||
var result = IndicatorExtensions.GetInputValue(indicator, args, invalidType);
|
||||
|
||||
Assert.Equal(105, result.Value); // Should default to Close (105)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using TradingPlatform.BusinessLayer.Chart;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public enum SourceType
|
||||
@@ -88,6 +91,11 @@ public static class IndicatorExtensions
|
||||
}
|
||||
|
||||
#pragma warning disable CA1416 // Validate platform compatibility
|
||||
public static int GetHLineY(IChartWindowCoordinatesConverter converter, double value)
|
||||
{
|
||||
return (int)converter.GetChartY(value);
|
||||
}
|
||||
|
||||
public static void PaintHLine(this Indicator indicator, PaintChartEventArgs args, double value, Pen pen)
|
||||
{
|
||||
if (indicator.CurrentChart == null)
|
||||
@@ -101,7 +109,7 @@ public static class IndicatorExtensions
|
||||
gr.SetClip(clientRect);
|
||||
int leftX = clientRect.Left;
|
||||
int rightX = clientRect.Right;
|
||||
int Y = (int)converter.GetChartY(value);
|
||||
int Y = GetHLineY(converter, value);
|
||||
|
||||
using (pen)
|
||||
{
|
||||
@@ -109,6 +117,39 @@ public static class IndicatorExtensions
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Point> GetSmoothCurvePoints(Indicator indicator, IChartWindowCoordinatesConverter converter, Rectangle clientRect, LineSeries series)
|
||||
{
|
||||
if (indicator == null) throw new ArgumentNullException(nameof(indicator));
|
||||
if (converter == null) throw new ArgumentNullException(nameof(converter));
|
||||
var data = indicator.HistoricalData;
|
||||
if (data == null) return new List<Point>();
|
||||
|
||||
var lastTime = data.Time(data.Count - 1);
|
||||
var firstTime = data.Time(0);
|
||||
|
||||
IChartWindowCoordinatesConverter safeConverter = converter!;
|
||||
DateTime tLeft = safeConverter.GetTime(clientRect.Left);
|
||||
DateTime leftTime = tLeft > lastTime ? tLeft : lastTime;
|
||||
|
||||
DateTime tRight = safeConverter.GetTime(clientRect.Right);
|
||||
DateTime rightTime = tRight < firstTime ? tRight : firstTime;
|
||||
|
||||
int leftIndex = (int)data.GetIndexByTime(leftTime.Ticks) + 1;
|
||||
int rightIndex = (int)data.GetIndexByTime(rightTime.Ticks);
|
||||
|
||||
List<Point> allPoints = new List<Point>();
|
||||
|
||||
for (int i = rightIndex; i < leftIndex; i++)
|
||||
{
|
||||
int barX = (int)converter.GetChartX(data.Time(i));
|
||||
int barY = (int)converter.GetChartY(series[i]);
|
||||
int halfBarWidth = indicator.CurrentChart.BarsWidth / 2;
|
||||
Point point = new Point(barX + halfBarWidth, barY);
|
||||
allPoints.Add(point);
|
||||
}
|
||||
return allPoints;
|
||||
}
|
||||
|
||||
public static void PaintSmoothCurve(this Indicator indicator, PaintChartEventArgs args, LineSeries series, int warmupPeriod, bool showColdValues = true, double tension = 0.2)
|
||||
{
|
||||
if (!series.Visible || indicator.CurrentChart == null)
|
||||
@@ -121,26 +162,13 @@ public static class IndicatorExtensions
|
||||
var clientRect = mainWindow.ClientRectangle;
|
||||
|
||||
gr.SetClip(clientRect);
|
||||
DateTime leftTime = new[] { converter.GetTime(clientRect.Left), indicator.HistoricalData.Time(indicator!.Count - 1) }.Max();
|
||||
DateTime rightTime = new[] { converter.GetTime(clientRect.Right), indicator.HistoricalData.Time(0) }.Min();
|
||||
|
||||
int leftIndex = (int)indicator.HistoricalData.GetIndexByTime(leftTime.Ticks) + 1;
|
||||
int rightIndex = (int)indicator.HistoricalData.GetIndexByTime(rightTime.Ticks);
|
||||
|
||||
List<Point> allPoints = new List<Point>();
|
||||
|
||||
for (int i = rightIndex; i < leftIndex; i++)
|
||||
{
|
||||
int barX = (int)converter.GetChartX(indicator.HistoricalData.Time(i));
|
||||
int barY = (int)converter.GetChartY(series[i]);
|
||||
int halfBarWidth = indicator.CurrentChart.BarsWidth / 2;
|
||||
Point point = new Point(barX + halfBarWidth, barY);
|
||||
allPoints.Add(point);
|
||||
}
|
||||
|
||||
List<Point> allPoints = GetSmoothCurvePoints(indicator, converter, clientRect, series);
|
||||
|
||||
if (allPoints.Count > 1)
|
||||
{
|
||||
if (allPoints.Count < 2) return;
|
||||
DateTime rightTime = new[] { converter.GetTime(clientRect.Right), indicator.HistoricalData.Time(0) }.Min();
|
||||
int rightIndex = (int)indicator.HistoricalData.GetIndexByTime(rightTime.Ticks);
|
||||
|
||||
using (Pen defaultPen = new(series.Color, series.Width) { DashStyle = ConvertLineStyleToDashStyle(series.Style) })
|
||||
using (Pen coldPen = new(series.Color, series.Width) { DashStyle = DashStyle.Dot })
|
||||
@@ -164,6 +192,47 @@ public static class IndicatorExtensions
|
||||
}
|
||||
}
|
||||
|
||||
public static List<(Rectangle Rect, Color Color)> GetHistogramRectangles(Indicator indicator, IChartWindowCoordinatesConverter converter, Rectangle clientRect, LineSeries series)
|
||||
{
|
||||
if (indicator == null) throw new ArgumentNullException(nameof(indicator));
|
||||
if (converter == null) throw new ArgumentNullException(nameof(converter));
|
||||
var data = indicator.HistoricalData;
|
||||
if (data == null) return new List<(Rectangle, Color)>();
|
||||
|
||||
var lastTime = data.Time(data.Count - 1);
|
||||
var firstTime = data.Time(0);
|
||||
|
||||
IChartWindowCoordinatesConverter safeConverter = converter!;
|
||||
DateTime tLeft = safeConverter.GetTime(clientRect.Left);
|
||||
DateTime leftTime = tLeft > lastTime ? tLeft : lastTime;
|
||||
|
||||
DateTime tRight = safeConverter.GetTime(clientRect.Right);
|
||||
DateTime rightTime = tRight < firstTime ? tRight : firstTime;
|
||||
|
||||
int leftIndex = (int)data.GetIndexByTime(leftTime.Ticks) + 1;
|
||||
int rightIndex = (int)data.GetIndexByTime(rightTime.Ticks);
|
||||
|
||||
var result = new List<(Rectangle, Color)>();
|
||||
|
||||
for (int i = rightIndex; i < leftIndex; i++)
|
||||
{
|
||||
int barX = (int)converter.GetChartX(data.Time(i));
|
||||
int barY = (int)converter.GetChartY(series[i]);
|
||||
int barY0 = (int)converter.GetChartY(0);
|
||||
int HistBarWidth = indicator.CurrentChart.BarsWidth - 2;
|
||||
|
||||
if (series[i] > 0)
|
||||
{
|
||||
result.Add((new Rectangle(barX, barY, HistBarWidth, Math.Abs(barY - barY0)), Color.FromArgb(150, 0, 255, 0)));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add((new Rectangle(barX, barY0, HistBarWidth, Math.Abs(barY0 - barY)), Color.FromArgb(150, 255, 0, 0)));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void PaintHistogram(this Indicator indicator, PaintChartEventArgs args, LineSeries series, int warmupPeriod, bool showColdValues = true)
|
||||
{
|
||||
if (!series.Visible || indicator.CurrentChart == null)
|
||||
@@ -176,32 +245,14 @@ public static class IndicatorExtensions
|
||||
var clientRect = mainWindow.ClientRectangle;
|
||||
|
||||
gr.SetClip(clientRect);
|
||||
DateTime leftTime = new[] { converter.GetTime(clientRect.Left), indicator.HistoricalData.Time(indicator!.Count - 1) }.Max();
|
||||
DateTime rightTime = new[] { converter.GetTime(clientRect.Right), indicator.HistoricalData.Time(0) }.Min();
|
||||
|
||||
var rects = GetHistogramRectangles(indicator, converter, clientRect, series);
|
||||
|
||||
int leftIndex = (int)indicator.HistoricalData.GetIndexByTime(leftTime.Ticks) + 1;
|
||||
int rightIndex = (int)indicator.HistoricalData.GetIndexByTime(rightTime.Ticks);
|
||||
|
||||
for (int i = rightIndex; i < leftIndex; i++)
|
||||
foreach (var (rect, color) in rects)
|
||||
{
|
||||
int barX = (int)converter.GetChartX(indicator.HistoricalData.Time(i));
|
||||
int barY = (int)converter.GetChartY(series[i]);
|
||||
int barY0 = (int)converter.GetChartY(0);
|
||||
int HistBarWidth = indicator.CurrentChart.BarsWidth - 2;
|
||||
|
||||
if (series[i] > 0)
|
||||
using (Brush hist = new SolidBrush(color))
|
||||
{
|
||||
using (Brush hist = new SolidBrush(Color.FromArgb(150, 0, 255, 0)))
|
||||
{
|
||||
gr.FillRectangle(hist, barX, barY, HistBarWidth, Math.Abs(barY - barY0));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using (Brush hist = new SolidBrush(Color.FromArgb(150, 255, 0, 0)))
|
||||
{
|
||||
gr.FillRectangle(hist, barX, barY0, HistBarWidth, Math.Abs(barY0 - barY));
|
||||
}
|
||||
gr.FillRectangle(hist, rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
|
||||
using System.Drawing;
|
||||
|
||||
namespace TradingPlatform.BusinessLayer;
|
||||
namespace TradingPlatform.BusinessLayer
|
||||
{
|
||||
using TradingPlatform.BusinessLayer.Chart;
|
||||
|
||||
#region Enums
|
||||
#region Enums
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the style of indicator line.
|
||||
@@ -185,7 +187,7 @@ public class HistoricalData
|
||||
for (int i = 0; i < _items.Count; i++)
|
||||
{
|
||||
if (_items[i].TicksLeft == ticks)
|
||||
return i;
|
||||
return Count - 1 - i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@@ -354,60 +356,68 @@ public class PaintChartEventArgs : EventArgs
|
||||
|
||||
#endregion
|
||||
|
||||
#region Chart
|
||||
|
||||
/// <summary>
|
||||
/// Chart interface
|
||||
/// </summary>
|
||||
public interface IChart
|
||||
{
|
||||
ChartWindow MainWindow { get; }
|
||||
ChartWindow[] Windows { get; }
|
||||
int BarsWidth { get; }
|
||||
#region Chart
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Chart window
|
||||
/// </summary>
|
||||
public class ChartWindow
|
||||
namespace TradingPlatform.BusinessLayer.Chart
|
||||
{
|
||||
public Rectangle ClientRectangle { get; set; }
|
||||
public ICoordinatesConverter CoordinatesConverter { get; set; } = new MockCoordinatesConverter();
|
||||
/// <summary>
|
||||
/// Coordinates converter interface
|
||||
/// </summary>
|
||||
public interface IChartWindowCoordinatesConverter
|
||||
{
|
||||
DateTime GetTime(int x);
|
||||
double GetChartX(DateTime time);
|
||||
double GetChartY(double value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Coordinates converter interface
|
||||
/// </summary>
|
||||
public interface ICoordinatesConverter
|
||||
namespace TradingPlatform.BusinessLayer
|
||||
{
|
||||
DateTime GetTime(int x);
|
||||
double GetChartX(DateTime time);
|
||||
double GetChartY(double value);
|
||||
}
|
||||
using TradingPlatform.BusinessLayer.Chart;
|
||||
|
||||
/// <summary>
|
||||
/// Mock coordinates converter
|
||||
/// </summary>
|
||||
public class MockCoordinatesConverter : ICoordinatesConverter
|
||||
{
|
||||
public DateTime GetTime(int x) => DateTime.UtcNow;
|
||||
public double GetChartX(DateTime time) => 0;
|
||||
public double GetChartY(double value) => 0;
|
||||
}
|
||||
/// <summary>
|
||||
/// Chart interface
|
||||
/// </summary>
|
||||
public interface IChart
|
||||
{
|
||||
ChartWindow MainWindow { get; }
|
||||
ChartWindow[] Windows { get; }
|
||||
int BarsWidth { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mock chart for testing
|
||||
/// </summary>
|
||||
public class MockChart : IChart
|
||||
{
|
||||
public ChartWindow MainWindow { get; } = new();
|
||||
public ChartWindow[] Windows { get; } = new[] { new ChartWindow() };
|
||||
public int BarsWidth { get; set; } = 10;
|
||||
}
|
||||
/// <summary>
|
||||
/// Chart window
|
||||
/// </summary>
|
||||
public class ChartWindow
|
||||
{
|
||||
public Rectangle ClientRectangle { get; set; }
|
||||
public IChartWindowCoordinatesConverter CoordinatesConverter { get; set; } = new MockCoordinatesConverter();
|
||||
}
|
||||
|
||||
#endregion
|
||||
/// <summary>
|
||||
/// Mock coordinates converter
|
||||
/// </summary>
|
||||
public class MockCoordinatesConverter : IChartWindowCoordinatesConverter
|
||||
{
|
||||
public DateTime GetTime(int x) => DateTime.UtcNow;
|
||||
public double GetChartX(DateTime time) => 0;
|
||||
public double GetChartY(double value) => 0;
|
||||
}
|
||||
|
||||
#region Indicator Base
|
||||
/// <summary>
|
||||
/// Mock chart for testing
|
||||
/// </summary>
|
||||
public class MockChart : IChart
|
||||
{
|
||||
public ChartWindow MainWindow { get; } = new();
|
||||
public ChartWindow[] Windows { get; } = new[] { new ChartWindow() };
|
||||
public int BarsWidth { get; set; } = 10;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Indicator Base
|
||||
|
||||
/// <summary>
|
||||
/// Watchlist indicator interface
|
||||
@@ -488,3 +498,4 @@ public abstract class Indicator
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user