Files
QuanTAlib/lib/numerics/agc/Agc.Tests.cs
T
Miha Kralj 7253f61299 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.
2026-02-21 20:45:38 -08:00

431 lines
12 KiB
C#

namespace QuanTAlib;
public class AgcTests
{
// Helper: generate a sine wave that oscillates around zero
private static TSeries MakeSineWave(int count, double amplitude = 1.0, double period = 20.0)
{
var series = new TSeries();
DateTime t = DateTime.UtcNow;
for (int i = 0; i < count; i++)
{
double val = amplitude * Math.Sin(2.0 * Math.PI * i / period);
series.Add(new TValue(t.AddMinutes(i), val));
}
return series;
}
// --- A) Constructor Validation ---
[Fact]
public void Constructor_ValidatesDecay_TooLow()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Agc(decay: 0.0));
Assert.Throws<ArgumentOutOfRangeException>(() => new Agc(decay: -0.5));
}
[Fact]
public void Constructor_ValidatesDecay_TooHigh()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Agc(decay: 1.0));
Assert.Throws<ArgumentOutOfRangeException>(() => new Agc(decay: 1.5));
}
[Fact]
public void Constructor_SetsName()
{
var ind = new Agc(0.991);
Assert.Equal("AGC(0.991)", ind.Name);
}
[Fact]
public void Constructor_SetsWarmupPeriod()
{
var ind = new Agc(0.991);
Assert.Equal(1, ind.WarmupPeriod);
}
[Fact]
public void Constructor_DefaultParameters()
{
var ind = new Agc();
Assert.Equal(0.991, ind.Decay);
}
// --- B) Basic Calculation ---
[Fact]
public void Calc_ReturnsFiniteValue()
{
var ind = new Agc();
// Feed an oscillating value (not raw price!)
var result = ind.Update(new TValue(DateTime.UtcNow, 0.5));
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Calc_PropertiesAccessible()
{
var ind = new Agc();
ind.Update(new TValue(DateTime.UtcNow, 0.5));
Assert.True(double.IsFinite(ind.Last.Value));
Assert.True(ind.IsHot);
Assert.Equal("AGC(0.991)", ind.Name);
_ = ind.IsNew;
}
[Fact]
public void SineInput_OutputBounded()
{
// A pure sine wave fed through AGC should produce output in [-1, +1]
var ind = new Agc(0.991);
var sine = MakeSineWave(500);
foreach (var item in sine)
{
var result = ind.Update(item);
Assert.True(result.Value >= -1.0001 && result.Value <= 1.0001,
$"AGC output {result.Value} exceeds [-1, +1] bounds");
}
}
[Fact]
public void ConstantInput_ReturnsOne()
{
// Constant positive input → peak = val → output = val/val = 1.0
var ind = new Agc(0.991);
double lastVal = 0;
for (int i = 0; i < 200; i++)
{
lastVal = ind.Update(new TValue(DateTime.UtcNow, 5.0)).Value;
}
Assert.Equal(1.0, lastVal, 1e-6);
}
[Fact]
public void ZeroInput_ReturnsZero()
{
// Zero input → output = 0 / peak = 0
var ind = new Agc(0.991);
ind.Update(new TValue(DateTime.UtcNow, 1.0)); // prime with non-zero
double val = ind.Update(new TValue(DateTime.UtcNow, 0.0)).Value;
Assert.Equal(0.0, val, 1e-10);
}
// --- C) State + Bar Correction ---
[Fact]
public void Calc_IsNew_AcceptsParameter()
{
var ind = new Agc();
var sine = MakeSineWave(20);
foreach (var item in sine)
{
ind.Update(item);
}
double val1 = ind.Last.Value;
ind.Update(new TValue(DateTime.UtcNow, 0.75), isNew: false);
double val2 = ind.Last.Value;
Assert.NotEqual(val1, val2);
}
[Fact]
public void Calc_IsNew_False_UpdatesValue()
{
var ind = new Agc();
ind.Update(new TValue(DateTime.UtcNow, 0.5), isNew: true);
ind.Update(new TValue(DateTime.UtcNow, 0.8), isNew: true);
double val1 = ind.Last.Value;
ind.Update(new TValue(DateTime.UtcNow, 0.3), isNew: false);
double val2 = ind.Last.Value;
Assert.NotEqual(val1, val2);
}
[Fact]
public void IterativeCorrections_RestoreToOriginalState()
{
var ind = new Agc();
var sine = MakeSineWave(50);
for (int i = 0; i < sine.Count; i++)
{
ind.Update(sine[i]);
}
double originalValue = ind.Last.Value;
// Feed corrections with isNew=false
ind.Update(new TValue(DateTime.UtcNow, 0.1), isNew: false);
ind.Update(new TValue(DateTime.UtcNow, 0.9), isNew: false);
ind.Update(new TValue(DateTime.UtcNow, -0.5), isNew: false);
// Restore with original last value
ind.Update(sine[^1], isNew: false);
double restoredValue = ind.Last.Value;
Assert.Equal(originalValue, restoredValue, 10);
}
[Fact]
public void Reset_ClearsState()
{
var ind = new Agc();
var sine = MakeSineWave(50);
foreach (var item in sine)
{
ind.Update(item);
}
ind.Reset();
var ind2 = new Agc();
var result1 = ind.Update(new TValue(DateTime.UtcNow, 0.5));
var result2 = ind2.Update(new TValue(DateTime.UtcNow, 0.5));
Assert.Equal(result2.Value, result1.Value, 10);
}
// --- D) Warmup/Convergence ---
[Fact]
public void IsHot_TrueAfterFirstUpdate()
{
var ind = new Agc();
Assert.False(ind.IsHot); // No data yet
ind.Update(new TValue(DateTime.UtcNow, 0.5));
Assert.True(ind.IsHot); // One bar is enough
}
// --- E) Robustness ---
[Fact]
public void NaN_Input_UsesLastValidValue()
{
var ind = new Agc();
ind.Update(new TValue(DateTime.UtcNow, 0.5));
ind.Update(new TValue(DateTime.UtcNow, 0.8));
var result = ind.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Infinity_Input_UsesLastValidValue()
{
var ind = new Agc();
ind.Update(new TValue(DateTime.UtcNow, 0.5));
ind.Update(new TValue(DateTime.UtcNow, 0.8));
var result = ind.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
Assert.True(double.IsFinite(result.Value));
var result2 = ind.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
Assert.True(double.IsFinite(result2.Value));
}
[Fact]
public void MultipleNaN_ContinuesWithLastValid()
{
var ind = new Agc();
ind.Update(new TValue(DateTime.UtcNow, 0.5));
ind.Update(new TValue(DateTime.UtcNow, 0.8));
for (int i = 0; i < 10; i++)
{
var result = ind.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsFinite(result.Value));
}
}
[Fact]
public void BatchCalc_HandlesNaN()
{
double[] input = [0.5, 0.8, double.NaN, -0.3, double.NaN, 0.6];
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}] should be finite");
}
}
// --- F) Consistency ---
[Fact]
public void AllModes_ProduceSameResult()
{
const double decay = 0.991;
var sine = MakeSineWave(200);
// 1. Span Mode
double[] spanOutput = new double[sine.Count];
Agc.Batch(sine.Values.ToArray(), spanOutput, decay);
// 2. TSeries Batch Mode
var agcBatch = new Agc(decay);
var batchResult = agcBatch.Update(sine);
// 3. Streaming Mode
var agcStream = new Agc(decay);
var streamResults = new List<double>();
foreach (var item in sine)
{
streamResults.Add(agcStream.Update(item).Value);
}
// 4. Eventing Mode
var pubSource = new TSeries();
var agcEvent = new Agc(pubSource, decay);
for (int i = 0; i < sine.Count; i++)
{
pubSource.Add(sine[i]);
}
// Assert all modes match
for (int i = 0; i < sine.Count; i++)
{
Assert.Equal(spanOutput[i], batchResult[i].Value, 1e-9);
Assert.Equal(spanOutput[i], streamResults[i], 1e-9);
}
Assert.Equal(spanOutput[^1], agcEvent.Last.Value, 1e-9);
}
// --- G) Span API ---
[Fact]
public void SpanCalc_ValidatesLength()
{
double[] source = new double[10];
double[] output = new double[5]; // Mismatched!
Assert.Throws<ArgumentException>(() => Agc.Batch(source, output));
}
[Fact]
public void SpanCalc_SineInput_OutputBounded()
{
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[] output = new double[500];
Agc.Batch(input, output, 0.991);
for (int i = 0; i < output.Length; i++)
{
Assert.True(output[i] >= -1.0001 && output[i] <= 1.0001,
$"Output[{i}] = {output[i]} exceeds [-1, +1] bounds");
}
}
[Fact]
public void SpanCalc_MatchesTSeriesCalc()
{
var sine = MakeSineWave(200);
// Span
double[] spanOutput = new double[sine.Count];
Agc.Batch(sine.Values.ToArray(), spanOutput, 0.991);
// TSeries
var ind = new Agc(0.991);
var tseriesResult = ind.Update(sine);
for (int i = 0; i < sine.Count; i++)
{
Assert.Equal(spanOutput[i], tseriesResult[i].Value, 1e-9);
}
}
// --- H) Chainability ---
[Fact]
public void Pub_FiresOnUpdate()
{
var ind = new Agc();
int fireCount = 0;
ind.Pub += (object? _, in TValueEventArgs _) => fireCount++;
ind.Update(new TValue(DateTime.UtcNow, 0.5));
ind.Update(new TValue(DateTime.UtcNow, 0.8));
Assert.Equal(2, fireCount);
}
[Fact]
public void EventChaining_Works()
{
var source = new TSeries();
var ind = new Agc(source);
source.Add(new TValue(DateTime.UtcNow, 0.5));
source.Add(new TValue(DateTime.UtcNow, 0.8));
Assert.True(double.IsFinite(ind.Last.Value));
}
// --- Additional ---
[Fact]
public void DifferentDecays_ProduceDifferentResults()
{
var sine = MakeSineWave(200);
var ind1 = new Agc(0.991);
var ind2 = new Agc(0.95);
foreach (var item in sine)
{
ind1.Update(item);
ind2.Update(item);
}
Assert.NotEqual(ind1.Last.Value, ind2.Last.Value);
}
[Fact]
public void LargeDataset_DoesNotThrow()
{
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);
Assert.True(double.IsFinite(output[^1]));
}
[Fact]
public void NegativeInput_ProducesNegativeOutput()
{
var ind = new Agc();
ind.Update(new TValue(DateTime.UtcNow, 1.0)); // prime peak
double val = ind.Update(new TValue(DateTime.UtcNow, -0.5)).Value;
Assert.True(val < 0, $"Negative input should produce negative output, got {val}");
}
[Fact]
public void Dispose_UnsubscribesFromSource()
{
var source = new TSeries();
var ind = new Agc(source);
source.Add(new TValue(DateTime.UtcNow, 0.5));
Assert.True(double.IsFinite(ind.Last.Value));
ind.Dispose();
// After dispose, further adds should not affect ind
double lastBefore = ind.Last.Value;
source.Add(new TValue(DateTime.UtcNow, 999.0));
Assert.Equal(lastBefore, ind.Last.Value, 10);
}
}