mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-24 13:38:05 +00:00
Add TRAMA implementation and comprehensive tests
- Implemented the TRAMA (Trend Regularity Adaptive Moving Average) class with adaptive EMA logic. - Added unit tests for TRAMA functionality, including constructor validation, basic calculations, state management, and robustness checks. - Created validation tests to ensure consistency across different modes of operation (streaming, batch, and static calculations). - Enhanced documentation for TRAMA, including performance profiles and quality metrics. - Updated workspace configuration by removing unnecessary folder references.
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for the AGC (Automatic Gain Control) filter.
|
||||
/// Since AGC is a proprietary Ehlers normalizer, no external library implementations exist.
|
||||
/// Validation uses self-consistency: bounded output, normalization behavior, mode consistency, and determinism.
|
||||
/// </summary>
|
||||
public class AgcValidationTests
|
||||
{
|
||||
[Fact]
|
||||
public void Validate_SineWave_NormalizesToUnitAmplitude()
|
||||
{
|
||||
// A pure sine wave (amplitude=1) should normalize to ~1 peak after warmup
|
||||
const int T = 1000;
|
||||
double[] sine = new double[T];
|
||||
for (int i = 0; i < T; i++)
|
||||
{
|
||||
sine[i] = Math.Sin(2.0 * Math.PI * i / 20.0);
|
||||
}
|
||||
|
||||
double[] output = new double[T];
|
||||
Agc.Batch(sine, output, 0.991);
|
||||
|
||||
// After warmup, output peaks should be close to ±1
|
||||
double maxAbs = 0;
|
||||
for (int i = T - 100; i < T; i++)
|
||||
{
|
||||
maxAbs = Math.Max(maxAbs, Math.Abs(output[i]));
|
||||
}
|
||||
Assert.True(maxAbs >= 0.95 && maxAbs <= 1.0001,
|
||||
$"Normalized sine should peak near ±1, got max |output| = {maxAbs}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_GrowingAmplitude_TracksWithinBounds()
|
||||
{
|
||||
// Sine wave with growing amplitude — AGC should keep output bounded
|
||||
const int T = 1000;
|
||||
double[] input = new double[T];
|
||||
for (int i = 0; i < T; i++)
|
||||
{
|
||||
double amplitude = 1.0 + i * 0.01; // grows from 1 to 11
|
||||
input[i] = amplitude * Math.Sin(2.0 * Math.PI * i / 20.0);
|
||||
}
|
||||
|
||||
double[] output = new double[T];
|
||||
Agc.Batch(input, output, 0.991);
|
||||
|
||||
for (int i = 0; i < T; i++)
|
||||
{
|
||||
Assert.True(output[i] >= -1.0001 && output[i] <= 1.0001,
|
||||
$"Output[{i}] = {output[i]} exceeds [-1, +1] bounds");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_StreamingMatchesSpan()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
var data = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Use roofing to create oscillating input
|
||||
double[] prices = data.Close.Values.ToArray();
|
||||
double[] filtered = new double[prices.Length];
|
||||
Roofing.Batch(prices, filtered, 48, 10);
|
||||
|
||||
// Span mode
|
||||
double[] spanOut = new double[filtered.Length];
|
||||
Agc.Batch(filtered, spanOut, 0.991);
|
||||
|
||||
// Streaming mode
|
||||
var ind = new Agc(0.991);
|
||||
double[] streamOut = new double[filtered.Length];
|
||||
for (int i = 0; i < filtered.Length; i++)
|
||||
{
|
||||
streamOut[i] = ind.Update(new TValue(DateTime.UtcNow, filtered[i])).Value;
|
||||
}
|
||||
|
||||
for (int i = 0; i < filtered.Length; i++)
|
||||
{
|
||||
Assert.Equal(spanOut[i], streamOut[i], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Deterministic()
|
||||
{
|
||||
double[] input = new double[500];
|
||||
for (int i = 0; i < input.Length; i++)
|
||||
{
|
||||
input[i] = Math.Sin(2.0 * Math.PI * i / 25.0) * (1.0 + 0.3 * Math.Sin(2.0 * Math.PI * i / 100.0));
|
||||
}
|
||||
|
||||
double[] out1 = new double[input.Length];
|
||||
double[] out2 = new double[input.Length];
|
||||
|
||||
Agc.Batch(input, out1, 0.991);
|
||||
Agc.Batch(input, out2, 0.991);
|
||||
|
||||
for (int i = 0; i < input.Length; i++)
|
||||
{
|
||||
Assert.Equal(out1[i], out2[i], 15);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_DecayingAmplitude_OutputGrows()
|
||||
{
|
||||
// When amplitude decays, AGC peak decays too, so normalized output stays near ±1
|
||||
const int T = 1000;
|
||||
double[] input = new double[T];
|
||||
for (int i = 0; i < T; i++)
|
||||
{
|
||||
double amplitude = 10.0 * Math.Exp(-i * 0.005); // exponentially decaying
|
||||
input[i] = amplitude * Math.Sin(2.0 * Math.PI * i / 20.0);
|
||||
}
|
||||
|
||||
double[] output = new double[T];
|
||||
Agc.Batch(input, output, 0.991);
|
||||
|
||||
// Output should still oscillate near ±1 in the tail (AGC adapts)
|
||||
double maxTail = 0;
|
||||
for (int i = T - 100; i < T; i++)
|
||||
{
|
||||
maxTail = Math.Max(maxTail, Math.Abs(output[i]));
|
||||
}
|
||||
Assert.True(maxTail > 0.5, $"Decaying amplitude should still produce sizable normalized output, got max = {maxTail}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_LargeDataset_Stable()
|
||||
{
|
||||
double[] input = new double[10000];
|
||||
for (int i = 0; i < input.Length; i++)
|
||||
{
|
||||
input[i] = Math.Sin(2.0 * Math.PI * i / 20.0);
|
||||
}
|
||||
double[] output = new double[input.Length];
|
||||
|
||||
Agc.Batch(input, output, 0.991);
|
||||
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]), $"Output[{i}] is not finite: {output[i]}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_NaN_Batch_Safe()
|
||||
{
|
||||
double[] input = new double[100];
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
input[i] = i % 7 == 0 ? double.NaN : Math.Sin(2.0 * Math.PI * i / 20.0);
|
||||
}
|
||||
double[] output = new double[100];
|
||||
|
||||
Agc.Batch(input, output, 0.991);
|
||||
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]), $"Output[{i}] should be finite with NaN input");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_DifferentDecays_ProduceDifferentOutput()
|
||||
{
|
||||
double[] input = new double[500];
|
||||
for (int i = 0; i < input.Length; i++)
|
||||
{
|
||||
input[i] = Math.Sin(2.0 * Math.PI * i / 20.0);
|
||||
}
|
||||
|
||||
double[] out1 = new double[input.Length];
|
||||
double[] out2 = new double[input.Length];
|
||||
|
||||
Agc.Batch(input, out1, 0.991);
|
||||
Agc.Batch(input, out2, 0.95);
|
||||
|
||||
bool anyDifferent = false;
|
||||
for (int i = 50; i < input.Length; i++)
|
||||
{
|
||||
if (Math.Abs(out1[i] - out2[i]) > 1e-10)
|
||||
{
|
||||
anyDifferent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Assert.True(anyDifferent, "Different decay parameters should produce different output");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user