mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-05 04:27:43 +00:00
060649192f
- Remove 'C# Implementation Considerations' sections from 34 indicator .md files - Delete 29 temp PowerShell scripts (_fix_mojibake.ps1, _hex_scan.ps1, etc.) - Move test files into tests/ subdirectories for consistent project structure - Add trader-focused bullet points to indicator documentation
771 lines
24 KiB
C#
771 lines
24 KiB
C#
namespace QuanTAlib.Tests;
|
|
|
|
public class ApzTests
|
|
{
|
|
private readonly GBM _gbm;
|
|
private readonly TBarSeries _bars;
|
|
|
|
public ApzTests()
|
|
{
|
|
_gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
|
_bars = _gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
}
|
|
|
|
#region Constructor Tests
|
|
|
|
[Fact]
|
|
public void Constructor_ValidatesInput()
|
|
{
|
|
Assert.Throws<ArgumentException>(() => new Apz(0));
|
|
Assert.Throws<ArgumentException>(() => new Apz(-1));
|
|
Assert.Throws<ArgumentException>(() => new Apz(10, 0));
|
|
Assert.Throws<ArgumentException>(() => new Apz(10, -1));
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_ValidBoundaryValues()
|
|
{
|
|
var apz1 = new Apz(1);
|
|
Assert.NotNull(apz1);
|
|
Assert.Equal(1, apz1.WarmupPeriod);
|
|
|
|
var apz2 = new Apz(100, 0.5);
|
|
Assert.NotNull(apz2);
|
|
Assert.Equal(100, apz2.WarmupPeriod);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_SetsName()
|
|
{
|
|
var apz = new Apz(20, 2.5);
|
|
Assert.Contains("Apz", apz.Name, StringComparison.Ordinal);
|
|
Assert.Contains("20", apz.Name, StringComparison.Ordinal);
|
|
Assert.Contains("2.50", apz.Name, StringComparison.Ordinal);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Basic Functionality Tests
|
|
|
|
[Fact]
|
|
public void Calc_ReturnsValue()
|
|
{
|
|
var apz = new Apz(10);
|
|
var bar = _bars[0];
|
|
|
|
var result = apz.Update(bar);
|
|
|
|
Assert.True(double.IsFinite(result.Value));
|
|
Assert.Equal(result.Value, apz.Last.Value);
|
|
Assert.True(double.IsFinite(apz.Upper.Value));
|
|
Assert.True(double.IsFinite(apz.Lower.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void FirstValue_ReturnsExpected()
|
|
{
|
|
var apz = new Apz(10);
|
|
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 100, 1000);
|
|
|
|
var result = apz.Update(bar);
|
|
|
|
// First value should be close to the input price (with warmup compensation)
|
|
Assert.True(double.IsFinite(result.Value));
|
|
// Upper should be greater than middle
|
|
Assert.True(apz.Upper.Value > apz.Last.Value);
|
|
// Lower should be less than middle
|
|
Assert.True(apz.Lower.Value < apz.Last.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void Properties_Accessible()
|
|
{
|
|
var apz = new Apz(10);
|
|
|
|
Assert.Equal(0, apz.Last.Value);
|
|
Assert.False(apz.IsHot);
|
|
Assert.Contains("Apz", apz.Name, StringComparison.Ordinal);
|
|
Assert.Equal(10, apz.WarmupPeriod);
|
|
|
|
apz.Update(new TBar(DateTime.UtcNow, 100, 105, 95, 100, 1000));
|
|
Assert.NotEqual(0, apz.Last.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void BandRelationships_Maintained()
|
|
{
|
|
var apz = new Apz(10, 2.0);
|
|
|
|
for (int i = 0; i < 20; i++)
|
|
{
|
|
apz.Update(_bars[i]);
|
|
|
|
if (double.IsFinite(apz.Last.Value))
|
|
{
|
|
// Upper band should always be >= middle
|
|
Assert.True(apz.Upper.Value >= apz.Last.Value,
|
|
$"Upper {apz.Upper.Value} should be >= Middle {apz.Last.Value} at bar {i}");
|
|
// Lower band should always be <= middle
|
|
Assert.True(apz.Lower.Value <= apz.Last.Value,
|
|
$"Lower {apz.Lower.Value} should be <= Middle {apz.Last.Value} at bar {i}");
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region State Management & Bar Correction Tests
|
|
|
|
[Fact]
|
|
public void Calc_IsNew_AcceptsParameter()
|
|
{
|
|
var apz = new Apz(10);
|
|
|
|
apz.Update(_bars[0], isNew: true);
|
|
double value1 = apz.Last.Value;
|
|
|
|
apz.Update(_bars[1], isNew: true);
|
|
double value2 = apz.Last.Value;
|
|
|
|
Assert.NotEqual(value1, value2);
|
|
}
|
|
|
|
[Fact]
|
|
public void Calc_IsNew_False_UpdatesValue()
|
|
{
|
|
var apz = new Apz(10);
|
|
|
|
apz.Update(_bars[0]);
|
|
apz.Update(_bars[1], isNew: true);
|
|
double beforeUpdate = apz.Last.Value;
|
|
|
|
// Update same bar with different value
|
|
var modifiedBar = new TBar(_bars[1].Time, _bars[1].Open, _bars[1].High + 10, _bars[1].Low, _bars[1].Close + 5, _bars[1].Volume);
|
|
apz.Update(modifiedBar, isNew: false);
|
|
double afterUpdate = apz.Last.Value;
|
|
|
|
Assert.NotEqual(beforeUpdate, afterUpdate);
|
|
}
|
|
|
|
[Fact]
|
|
public void IterativeCorrections_RestoreToOriginalState()
|
|
{
|
|
var apz = new Apz(5);
|
|
|
|
// Feed 10 new bars
|
|
TBar tenthBar = default;
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
tenthBar = _bars[i];
|
|
apz.Update(tenthBar, isNew: true);
|
|
}
|
|
|
|
// Remember state after 10 bars
|
|
double stateAfterTen = apz.Last.Value;
|
|
double upperAfterTen = apz.Upper.Value;
|
|
double lowerAfterTen = apz.Lower.Value;
|
|
|
|
// Generate 9 corrections with isNew=false (different values)
|
|
for (int i = 0; i < 9; i++)
|
|
{
|
|
var correctionBar = new TBar(tenthBar.Time, tenthBar.Open + i, tenthBar.High + i * 2, tenthBar.Low - i, tenthBar.Close + i, tenthBar.Volume);
|
|
apz.Update(correctionBar, isNew: false);
|
|
}
|
|
|
|
// Feed the remembered 10th bar again with isNew=false
|
|
apz.Update(tenthBar, isNew: false);
|
|
|
|
// State should match the original state after 10 bars
|
|
Assert.Equal(stateAfterTen, apz.Last.Value, precision: 10);
|
|
Assert.Equal(upperAfterTen, apz.Upper.Value, precision: 10);
|
|
Assert.Equal(lowerAfterTen, apz.Lower.Value, precision: 10);
|
|
}
|
|
|
|
[Fact]
|
|
public void Reset_ClearsState()
|
|
{
|
|
var apz = new Apz(10);
|
|
|
|
apz.Update(_bars[0]);
|
|
apz.Update(_bars[1]);
|
|
_ = apz.Last.Value; // Verify value exists before reset
|
|
|
|
apz.Reset();
|
|
|
|
Assert.Equal(0, apz.Last.Value);
|
|
Assert.False(apz.IsHot);
|
|
|
|
// After reset, should accept new values
|
|
apz.Update(_bars[0]);
|
|
Assert.NotEqual(0, apz.Last.Value);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Warmup & Convergence Tests
|
|
|
|
[Fact]
|
|
public void IsHot_BecomesTrueEventually()
|
|
{
|
|
var apz = new Apz(5);
|
|
|
|
Assert.False(apz.IsHot);
|
|
|
|
// Feed enough data
|
|
for (int i = 0; i < 100; i++)
|
|
{
|
|
apz.Update(_bars[i]);
|
|
}
|
|
|
|
Assert.True(apz.IsHot);
|
|
}
|
|
|
|
[Fact]
|
|
public void IsHot_IsPeriodDependent()
|
|
{
|
|
int[] periods = [5, 10, 20, 50];
|
|
int[] stepsToHot = new int[periods.Length];
|
|
|
|
for (int p = 0; p < periods.Length; p++)
|
|
{
|
|
var apz = new Apz(periods[p]);
|
|
int steps = 0;
|
|
while (!apz.IsHot && steps < 500)
|
|
{
|
|
apz.Update(_bars[steps]);
|
|
steps++;
|
|
}
|
|
stepsToHot[p] = steps;
|
|
}
|
|
|
|
// Larger periods should take more steps to become hot
|
|
// (due to beta^2 decay being slower)
|
|
Assert.True(stepsToHot[0] <= stepsToHot[1]);
|
|
Assert.True(stepsToHot[1] <= stepsToHot[2]);
|
|
Assert.True(stepsToHot[2] <= stepsToHot[3]);
|
|
}
|
|
|
|
[Fact]
|
|
public void WarmupPeriod_IsSetCorrectly()
|
|
{
|
|
var apz = new Apz(25);
|
|
Assert.Equal(25, apz.WarmupPeriod);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Robustness (NaN/Infinity) Tests
|
|
|
|
[Fact]
|
|
public void NaN_Input_UsesLastValidValue()
|
|
{
|
|
var apz = new Apz(5);
|
|
|
|
apz.Update(new TBar(DateTime.UtcNow, 100, 105, 95, 100, 1000));
|
|
apz.Update(new TBar(DateTime.UtcNow, 110, 115, 105, 110, 1000));
|
|
|
|
var resultAfterNaN = apz.Update(new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, 1000));
|
|
|
|
Assert.True(double.IsFinite(resultAfterNaN.Value));
|
|
Assert.True(double.IsFinite(apz.Upper.Value));
|
|
Assert.True(double.IsFinite(apz.Lower.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Infinity_Input_UsesLastValidValue()
|
|
{
|
|
var apz = new Apz(5);
|
|
|
|
apz.Update(new TBar(DateTime.UtcNow, 100, 105, 95, 100, 1000));
|
|
apz.Update(new TBar(DateTime.UtcNow, 110, 115, 105, 110, 1000));
|
|
|
|
var resultAfterPosInf = apz.Update(new TBar(DateTime.UtcNow, double.PositiveInfinity, double.PositiveInfinity, double.PositiveInfinity, double.PositiveInfinity, 1000));
|
|
Assert.True(double.IsFinite(resultAfterPosInf.Value));
|
|
|
|
var resultAfterNegInf = apz.Update(new TBar(DateTime.UtcNow, double.NegativeInfinity, double.NegativeInfinity, double.NegativeInfinity, double.NegativeInfinity, 1000));
|
|
Assert.True(double.IsFinite(resultAfterNegInf.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void MultipleNaN_ContinuesWithLastValid()
|
|
{
|
|
var apz = new Apz(5);
|
|
|
|
apz.Update(new TBar(DateTime.UtcNow, 100, 105, 95, 100, 1000));
|
|
apz.Update(new TBar(DateTime.UtcNow, 110, 115, 105, 110, 1000));
|
|
apz.Update(new TBar(DateTime.UtcNow, 120, 125, 115, 120, 1000));
|
|
|
|
var r1 = apz.Update(new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, 1000));
|
|
var r2 = apz.Update(new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, 1000));
|
|
var r3 = apz.Update(new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, 1000));
|
|
|
|
Assert.True(double.IsFinite(r1.Value));
|
|
Assert.True(double.IsFinite(r2.Value));
|
|
Assert.True(double.IsFinite(r3.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void BatchCalc_HandlesNaN()
|
|
{
|
|
double[] high = [105, 115, double.NaN, 125, 135];
|
|
double[] low = [95, 105, double.NaN, 115, 125];
|
|
double[] close = [100, 110, double.NaN, 120, 130];
|
|
double[] middle = new double[5];
|
|
double[] upper = new double[5];
|
|
double[] lower = new double[5];
|
|
|
|
Apz.Batch(high, low, close, new Apz.BatchOutputs(middle, upper, lower), 3);
|
|
|
|
foreach (var val in middle)
|
|
{
|
|
Assert.True(double.IsFinite(val), $"Expected finite value but got {val}");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void FirstBar_AllNaN_ReturnsNaN()
|
|
{
|
|
var apz = new Apz(5);
|
|
|
|
var result = apz.Update(new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, 1000));
|
|
|
|
Assert.True(double.IsNaN(result.Value));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Consistency Tests
|
|
|
|
[Fact]
|
|
public void BatchCalc_MatchesIterativeCalc()
|
|
{
|
|
var apzIterative = new Apz(10);
|
|
var apzBatch = new Apz(10);
|
|
|
|
// Calculate iteratively
|
|
var iterativeMiddle = new List<double>();
|
|
var iterativeUpper = new List<double>();
|
|
var iterativeLower = new List<double>();
|
|
foreach (var bar in _bars)
|
|
{
|
|
apzIterative.Update(bar);
|
|
iterativeMiddle.Add(apzIterative.Last.Value);
|
|
iterativeUpper.Add(apzIterative.Upper.Value);
|
|
iterativeLower.Add(apzIterative.Lower.Value);
|
|
}
|
|
|
|
// Calculate batch
|
|
var (batchMiddle, batchUpper, batchLower) = apzBatch.Update(_bars);
|
|
|
|
// Compare
|
|
Assert.Equal(iterativeMiddle.Count, batchMiddle.Count);
|
|
for (int i = 0; i < iterativeMiddle.Count; i++)
|
|
{
|
|
Assert.Equal(iterativeMiddle[i], batchMiddle[i].Value, precision: 9);
|
|
Assert.Equal(iterativeUpper[i], batchUpper[i].Value, precision: 9);
|
|
Assert.Equal(iterativeLower[i], batchLower[i].Value, precision: 9);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void AllModes_ProduceSameResult()
|
|
{
|
|
const int period = 10;
|
|
double multiplier = 2.0;
|
|
|
|
// 1. Batch Mode (static method)
|
|
var (batchMiddle, batchUpper, batchLower) = Apz.Batch(_bars, period, multiplier);
|
|
double expectedMiddle = batchMiddle.Last.Value;
|
|
double expectedUpper = batchUpper.Last.Value;
|
|
double expectedLower = batchLower.Last.Value;
|
|
|
|
// 2. Span Mode (static method with spans)
|
|
double[] highArr = _bars.High.Values.ToArray();
|
|
double[] lowArr = _bars.Low.Values.ToArray();
|
|
double[] closeArr = _bars.Close.Values.ToArray();
|
|
double[] middleSpan = new double[_bars.Count];
|
|
double[] upperSpan = new double[_bars.Count];
|
|
double[] lowerSpan = new double[_bars.Count];
|
|
Apz.Batch(highArr, lowArr, closeArr, new Apz.BatchOutputs(middleSpan, upperSpan, lowerSpan), period, multiplier);
|
|
double spanMiddle = middleSpan[^1];
|
|
double spanUpper = upperSpan[^1];
|
|
double spanLower = lowerSpan[^1];
|
|
|
|
// 3. Streaming Mode (instance, one bar at a time)
|
|
var streamingApz = new Apz(period, multiplier);
|
|
foreach (var bar in _bars)
|
|
{
|
|
streamingApz.Update(bar);
|
|
}
|
|
double streamingMiddle = streamingApz.Last.Value;
|
|
double streamingUpper = streamingApz.Upper.Value;
|
|
double streamingLower = streamingApz.Lower.Value;
|
|
|
|
// 4. Eventing Mode (chained via TBarSeries)
|
|
var pubSource = new TBarSeries();
|
|
var eventingApz = new Apz(pubSource, period, multiplier);
|
|
foreach (var bar in _bars)
|
|
{
|
|
pubSource.Add(bar);
|
|
}
|
|
double eventingMiddle = eventingApz.Last.Value;
|
|
double eventingUpper = eventingApz.Upper.Value;
|
|
double eventingLower = eventingApz.Lower.Value;
|
|
|
|
// Assert all modes produce identical results
|
|
Assert.Equal(expectedMiddle, spanMiddle, precision: 9);
|
|
Assert.Equal(expectedMiddle, streamingMiddle, precision: 9);
|
|
Assert.Equal(expectedMiddle, eventingMiddle, precision: 9);
|
|
|
|
Assert.Equal(expectedUpper, spanUpper, precision: 9);
|
|
Assert.Equal(expectedUpper, streamingUpper, precision: 9);
|
|
Assert.Equal(expectedUpper, eventingUpper, precision: 9);
|
|
|
|
Assert.Equal(expectedLower, spanLower, precision: 9);
|
|
Assert.Equal(expectedLower, streamingLower, precision: 9);
|
|
Assert.Equal(expectedLower, eventingLower, precision: 9);
|
|
}
|
|
|
|
[Fact]
|
|
public void StaticBatch_Works()
|
|
{
|
|
var (middle, upper, lower) = Apz.Batch(_bars, 10);
|
|
|
|
Assert.Equal(_bars.Count, middle.Count);
|
|
Assert.Equal(_bars.Count, upper.Count);
|
|
Assert.Equal(_bars.Count, lower.Count);
|
|
Assert.True(double.IsFinite(middle.Last.Value));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Span API Tests
|
|
|
|
[Fact]
|
|
public void SpanBatch_ValidatesInput()
|
|
{
|
|
double[] high = [105, 115, 125];
|
|
double[] low = [95, 105, 115];
|
|
double[] close = [100, 110, 120];
|
|
double[] middle = new double[3];
|
|
double[] upper = new double[3];
|
|
double[] lower = new double[3];
|
|
double[] wrongSizeOutput = new double[2];
|
|
double[] wrongSizeInput = [100, 110];
|
|
|
|
// Period must be > 0
|
|
Assert.Throws<ArgumentException>(() =>
|
|
Apz.Batch(high, low, close, new Apz.BatchOutputs(middle, upper, lower), 0));
|
|
Assert.Throws<ArgumentException>(() =>
|
|
Apz.Batch(high, low, close, new Apz.BatchOutputs(middle, upper, lower), -1));
|
|
|
|
// Multiplier must be > 0
|
|
Assert.Throws<ArgumentException>(() =>
|
|
Apz.Batch(high, low, close, new Apz.BatchOutputs(middle, upper, lower), 3, 0));
|
|
Assert.Throws<ArgumentException>(() =>
|
|
Apz.Batch(high, low, close, new Apz.BatchOutputs(middle, upper, lower), 3, -1));
|
|
|
|
// Output must be same length as input
|
|
Assert.Throws<ArgumentException>(() =>
|
|
Apz.Batch(high, low, close, new Apz.BatchOutputs(wrongSizeOutput, upper, lower), 3));
|
|
|
|
// Input arrays must have same length
|
|
Assert.Throws<ArgumentException>(() =>
|
|
Apz.Batch(wrongSizeInput, low, close, new Apz.BatchOutputs(middle, upper, lower), 3));
|
|
}
|
|
|
|
[Fact]
|
|
public void SpanBatch_MatchesTSeriesBatch()
|
|
{
|
|
int period = 10;
|
|
double multiplier = 2.0;
|
|
|
|
var (tseriesMiddle, tseriesUpper, tseriesLower) = Apz.Batch(_bars, period, multiplier);
|
|
|
|
double[] highArr = _bars.High.Values.ToArray();
|
|
double[] lowArr = _bars.Low.Values.ToArray();
|
|
double[] closeArr = _bars.Close.Values.ToArray();
|
|
double[] spanMiddle = new double[_bars.Count];
|
|
double[] spanUpper = new double[_bars.Count];
|
|
double[] spanLower = new double[_bars.Count];
|
|
|
|
Apz.Batch(highArr, lowArr, closeArr, new Apz.BatchOutputs(spanMiddle, spanUpper, spanLower), period, multiplier);
|
|
|
|
for (int i = 0; i < _bars.Count; i++)
|
|
{
|
|
Assert.Equal(tseriesMiddle[i].Value, spanMiddle[i], precision: 10);
|
|
Assert.Equal(tseriesUpper[i].Value, spanUpper[i], precision: 10);
|
|
Assert.Equal(tseriesLower[i].Value, spanLower[i], precision: 10);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void SpanBatch_ZeroAllocation()
|
|
{
|
|
double[] high = new double[10000];
|
|
double[] low = new double[10000];
|
|
double[] close = new double[10000];
|
|
double[] middle = new double[10000];
|
|
double[] upper = new double[10000];
|
|
double[] lower = new double[10000];
|
|
|
|
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
|
for (int i = 0; i < high.Length; i++)
|
|
{
|
|
var bar = gbm.Next();
|
|
high[i] = bar.High;
|
|
low[i] = bar.Low;
|
|
close[i] = bar.Close;
|
|
}
|
|
|
|
// Warm up
|
|
Apz.Batch(high, low, close, new Apz.BatchOutputs(middle, upper, lower), 100);
|
|
|
|
// Verify method completes without OOM or stack overflow
|
|
Assert.True(double.IsFinite(middle[^1]));
|
|
Assert.True(double.IsFinite(upper[^1]));
|
|
Assert.True(double.IsFinite(lower[^1]));
|
|
}
|
|
|
|
[Fact]
|
|
public void SpanBatch_Period1_Works()
|
|
{
|
|
double[] high = [105, 115, 125];
|
|
double[] low = [95, 105, 115];
|
|
double[] close = [100, 110, 120];
|
|
double[] middle = new double[3];
|
|
double[] upper = new double[3];
|
|
double[] lower = new double[3];
|
|
|
|
Apz.Batch(high, low, close, new Apz.BatchOutputs(middle, upper, lower), 1);
|
|
|
|
foreach (var val in middle)
|
|
{
|
|
Assert.True(double.IsFinite(val));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void SpanBatch_EmptyInput_DoesNotThrow()
|
|
{
|
|
double[] high = [];
|
|
double[] low = [];
|
|
double[] close = [];
|
|
double[] middle = [];
|
|
double[] upper = [];
|
|
double[] lower = [];
|
|
|
|
// Should not throw
|
|
var ex = Record.Exception(() => Apz.Batch(high, low, close, new Apz.BatchOutputs(middle, upper, lower), 10));
|
|
Assert.Null(ex);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Prime Tests
|
|
|
|
[Fact]
|
|
public void Prime_SetsStateCorrectly()
|
|
{
|
|
var apz = new Apz(10);
|
|
apz.Prime(_bars);
|
|
|
|
Assert.True(apz.IsHot);
|
|
Assert.True(double.IsFinite(apz.Last.Value));
|
|
Assert.True(double.IsFinite(apz.Upper.Value));
|
|
Assert.True(double.IsFinite(apz.Lower.Value));
|
|
|
|
// Verify it continues correctly
|
|
var nextBar = _gbm.Next();
|
|
apz.Update(nextBar);
|
|
Assert.True(double.IsFinite(apz.Last.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Prime_WithEmptySeries_DoesNotThrow()
|
|
{
|
|
var apz = new Apz(10);
|
|
var emptySeries = new TBarSeries();
|
|
|
|
// Should not throw
|
|
apz.Prime(emptySeries);
|
|
|
|
Assert.False(apz.IsHot);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Calculate Method Tests
|
|
|
|
[Fact]
|
|
public void Calculate_ReturnsCorrectResultsAndHotIndicator()
|
|
{
|
|
var ((middle, upper, lower), indicator) = Apz.Calculate(_bars, 10);
|
|
|
|
// Check results
|
|
Assert.Equal(_bars.Count, middle.Count);
|
|
Assert.True(double.IsFinite(middle.Last.Value));
|
|
|
|
// Check indicator state
|
|
Assert.True(indicator.IsHot);
|
|
Assert.Equal(middle.Last.Value, indicator.Last.Value, precision: 10);
|
|
Assert.Equal(upper.Last.Value, indicator.Upper.Value, precision: 10);
|
|
Assert.Equal(lower.Last.Value, indicator.Lower.Value, precision: 10);
|
|
|
|
// Verify indicator continues correctly
|
|
var nextBar = _gbm.Next();
|
|
indicator.Update(nextBar);
|
|
Assert.True(double.IsFinite(indicator.Last.Value));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Chainability Tests
|
|
|
|
[Fact]
|
|
public void Chainability_Works()
|
|
{
|
|
var source = new TBarSeries();
|
|
var apz = new Apz(source, 10);
|
|
|
|
source.Add(new TBar(DateTime.UtcNow, 100, 105, 95, 100, 1000));
|
|
Assert.True(double.IsFinite(apz.Last.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Pub_EventFires()
|
|
{
|
|
var apz = new Apz(10);
|
|
bool eventFired = false;
|
|
apz.Pub += (object? sender, in TValueEventArgs args) => eventFired = true;
|
|
|
|
apz.Update(new TBar(DateTime.UtcNow, 100, 105, 95, 100, 1000));
|
|
Assert.True(eventFired);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Algorithm-Specific Tests
|
|
|
|
[Fact]
|
|
public void DoubleSmoothing_ProducesSmoothOutput()
|
|
{
|
|
// Use a larger period for more smoothing effect
|
|
var apz = new Apz(50);
|
|
var results = new List<double>();
|
|
|
|
// Feed volatile data - use more bars to allow convergence
|
|
for (int i = 0; i < 200; i++)
|
|
{
|
|
apz.Update(_bars[i]);
|
|
results.Add(apz.Last.Value);
|
|
}
|
|
|
|
// Calculate average change in output (skip warmup period)
|
|
int startIdx = 60; // Skip warmup
|
|
double sumChanges = 0;
|
|
for (int i = startIdx + 1; i < results.Count; i++)
|
|
{
|
|
sumChanges += Math.Abs(results[i] - results[i - 1]);
|
|
}
|
|
double avgChange = sumChanges / (results.Count - startIdx - 1);
|
|
|
|
// Calculate average change in input for same period
|
|
double sumInputChanges = 0;
|
|
for (int i = startIdx + 1; i < 200; i++)
|
|
{
|
|
sumInputChanges += Math.Abs(_bars[i].Close - _bars[i - 1].Close);
|
|
}
|
|
double avgInputChange = sumInputChanges / (200 - startIdx - 1);
|
|
|
|
// Double-smoothed output should be smoother than input
|
|
Assert.True(avgChange < avgInputChange,
|
|
$"Double-smoothed output ({avgChange:F4}) should be smoother than input ({avgInputChange:F4})");
|
|
}
|
|
|
|
[Fact]
|
|
public void SqrtPeriod_AffectsSmoothing()
|
|
{
|
|
// With sqrt(period), larger periods have proportionally less smoothing
|
|
// than standard EMA
|
|
|
|
var apz4 = new Apz(4); // sqrt(4) = 2, alpha = 2/(2+1) = 0.667
|
|
var apz100 = new Apz(100); // sqrt(100) = 10, alpha = 2/(10+1) = 0.182
|
|
|
|
// Feed same data
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
apz4.Update(_bars[i]);
|
|
apz100.Update(_bars[i]);
|
|
}
|
|
|
|
// Both should have finite values
|
|
Assert.True(double.IsFinite(apz4.Last.Value));
|
|
Assert.True(double.IsFinite(apz100.Last.Value));
|
|
|
|
// Period 100 should be smoother (closer to mean)
|
|
// and take longer to become hot
|
|
Assert.True(apz4.IsHot);
|
|
// Period 100 may or may not be hot after 50 bars
|
|
}
|
|
|
|
[Fact]
|
|
public void MultiplierAffectsBandWidth()
|
|
{
|
|
var apz1 = new Apz(10, 1.0);
|
|
var apz2 = new Apz(10, 2.0);
|
|
var apz3 = new Apz(10, 3.0);
|
|
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
apz1.Update(_bars[i]);
|
|
apz2.Update(_bars[i]);
|
|
apz3.Update(_bars[i]);
|
|
}
|
|
|
|
// All should have same middle
|
|
Assert.Equal(apz1.Last.Value, apz2.Last.Value, precision: 10);
|
|
Assert.Equal(apz2.Last.Value, apz3.Last.Value, precision: 10);
|
|
|
|
// Band widths should scale with multiplier
|
|
double width1 = apz1.Upper.Value - apz1.Lower.Value;
|
|
double width2 = apz2.Upper.Value - apz2.Lower.Value;
|
|
double width3 = apz3.Upper.Value - apz3.Lower.Value;
|
|
|
|
Assert.Equal(width2, width1 * 2, precision: 10);
|
|
Assert.Equal(width3, width1 * 3, precision: 10);
|
|
}
|
|
|
|
[Fact]
|
|
public void FlatLine_ReturnsSameMiddle()
|
|
{
|
|
var apz = new Apz(10);
|
|
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
apz.Update(new TBar(DateTime.UtcNow, 100, 105, 95, 100, 1000));
|
|
}
|
|
|
|
// Middle should converge to 100
|
|
Assert.Equal(100, apz.Last.Value, precision: 1);
|
|
}
|
|
|
|
[Fact]
|
|
public void ZeroRange_ProducesZeroBandWidth()
|
|
{
|
|
var apz = new Apz(10);
|
|
|
|
// Feed bars with no range (high = low = close)
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
apz.Update(new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1000));
|
|
}
|
|
|
|
// Bands should converge to middle (zero width)
|
|
Assert.Equal(apz.Last.Value, apz.Upper.Value, precision: 1);
|
|
Assert.Equal(apz.Last.Value, apz.Lower.Value, precision: 1);
|
|
}
|
|
|
|
#endregion
|
|
}
|