mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-14 08:38:04 +00:00
- Implemented Prime method in Vel, Ao, Apo, Frama, Adl, Adosc, Aobv, Cmf, Efi, Eom, Iii, Kvo, Mfi, Nvi, Obv, Pvd, Pvi, Pvo, Pvr, Pvt, Tvi, Twap, Va, Vf, Vo, Vroc, Vwad, Vwap, and Vwma classes. - The Prime method resets the indicator state and processes the provided historical bar data to initialize the indicator. - Added warmup period property to Adl and Wad classes to define the minimum number of data points required for validity. - Updated benchmark tests to use Batch methods for performance evaluation.
904 lines
27 KiB
C#
904 lines
27 KiB
C#
namespace QuanTAlib.Tests;
|
|
|
|
public class ApchannelTests
|
|
{
|
|
private const double Tolerance = 1e-10;
|
|
|
|
#region Constructor & Validation
|
|
|
|
[Fact]
|
|
public void Constructor_ValidatesInput()
|
|
{
|
|
// Alpha must be > 0
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => new Apchannel(0.0));
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => new Apchannel(-0.1));
|
|
|
|
// Alpha must be <= 1
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => new Apchannel(1.1));
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => new Apchannel(2.0));
|
|
|
|
// Valid construction
|
|
var apc = new Apchannel(0.2);
|
|
Assert.NotNull(apc);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_ValidBoundaryValues()
|
|
{
|
|
var apc1 = new Apchannel(0.001); // Very small alpha
|
|
Assert.NotNull(apc1);
|
|
|
|
var apc2 = new Apchannel(1.0); // Maximum alpha
|
|
Assert.NotNull(apc2);
|
|
|
|
var apc3 = new Apchannel(0.5); // Mid-range alpha
|
|
Assert.NotNull(apc3);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Basic Functionality
|
|
|
|
[Fact]
|
|
public void Calc_ReturnsValue()
|
|
{
|
|
var apc = new Apchannel(0.2);
|
|
var time = DateTime.UtcNow;
|
|
|
|
Assert.Equal(0, apc.Last.Value);
|
|
Assert.Equal(0, apc.UpperBand);
|
|
Assert.Equal(0, apc.LowerBand);
|
|
|
|
var bar = new TBar(time, 100, 105, 95, 100, 1000);
|
|
var result = apc.Add(bar);
|
|
|
|
Assert.True(result.Value > 0);
|
|
Assert.Equal(result.Value, apc.Last.Value);
|
|
Assert.Equal(105, apc.UpperBand, Tolerance);
|
|
Assert.Equal(95, apc.LowerBand, Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void FirstValue_ReturnsExpected()
|
|
{
|
|
var apc = new Apchannel(0.2);
|
|
var time = DateTime.UtcNow;
|
|
|
|
var bar = new TBar(time, 100, 110, 90, 100, 1000);
|
|
var result = apc.Add(bar);
|
|
|
|
// First bar: UpperBand = High, LowerBand = Low, Last = midpoint
|
|
Assert.Equal(110, apc.UpperBand, Tolerance);
|
|
Assert.Equal(90, apc.LowerBand, Tolerance);
|
|
Assert.Equal(100, result.Value, Tolerance); // (110 + 90) / 2
|
|
}
|
|
|
|
[Fact]
|
|
public void Properties_Accessible()
|
|
{
|
|
var apc = new Apchannel(0.3);
|
|
var time = DateTime.UtcNow;
|
|
|
|
Assert.Equal(0, apc.Last.Value);
|
|
Assert.False(apc.IsHot);
|
|
Assert.Contains("Apchannel", apc.Name, StringComparison.Ordinal);
|
|
Assert.Contains("0.3", apc.Name, StringComparison.Ordinal);
|
|
|
|
apc.Add(new TBar(time, 100, 105, 95, 100, 1000));
|
|
|
|
Assert.NotEqual(0, apc.Last.Value);
|
|
Assert.NotEqual(0, apc.UpperBand);
|
|
Assert.NotEqual(0, apc.LowerBand);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatesCorrectEma()
|
|
{
|
|
var apc = new Apchannel(0.5); // Alpha = 0.5 for easier manual calculation
|
|
var time = DateTime.UtcNow;
|
|
|
|
// Bar 1: High = 110, Low = 90
|
|
apc.Add(new TBar(time, 100, 110, 90, 100, 1000));
|
|
Assert.Equal(110, apc.UpperBand, Tolerance);
|
|
Assert.Equal(90, apc.LowerBand, Tolerance);
|
|
|
|
// Bar 2: High = 120, Low = 80
|
|
// UpperEMA = 0.5 * 110 + 0.5 * 120 = 115
|
|
// LowerEMA = 0.5 * 90 + 0.5 * 80 = 85
|
|
apc.Add(new TBar(time.AddMinutes(1), 100, 120, 80, 100, 1000));
|
|
Assert.Equal(115, apc.UpperBand, Tolerance);
|
|
Assert.Equal(85, apc.LowerBand, Tolerance);
|
|
|
|
// Bar 3: High = 130, Low = 70
|
|
// UpperEMA = 0.5 * 115 + 0.5 * 130 = 122.5
|
|
// LowerEMA = 0.5 * 85 + 0.5 * 70 = 77.5
|
|
apc.Add(new TBar(time.AddMinutes(2), 100, 130, 70, 100, 1000));
|
|
Assert.Equal(122.5, apc.UpperBand, Tolerance);
|
|
Assert.Equal(77.5, apc.LowerBand, Tolerance);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region State Management & Bar Correction
|
|
|
|
[Fact]
|
|
public void Calc_IsNew_AcceptsParameter()
|
|
{
|
|
var apc = new Apchannel(0.2);
|
|
var time = DateTime.UtcNow;
|
|
|
|
apc.Add(new TBar(time, 100, 105, 95, 100, 1000), isNew: true);
|
|
double value1 = apc.Last.Value;
|
|
|
|
apc.Add(new TBar(time.AddMinutes(1), 102, 108, 96, 102, 1000), isNew: true);
|
|
double value2 = apc.Last.Value;
|
|
|
|
Assert.NotEqual(value1, value2);
|
|
}
|
|
|
|
[Fact]
|
|
public void Calc_IsNew_False_UpdatesValue()
|
|
{
|
|
var apc = new Apchannel(0.2);
|
|
var time = DateTime.UtcNow;
|
|
|
|
apc.Add(new TBar(time, 100, 105, 95, 100, 1000));
|
|
apc.Add(new TBar(time.AddMinutes(1), 102, 108, 96, 102, 1000), isNew: true);
|
|
double beforeUpdate = apc.Last.Value;
|
|
|
|
apc.Add(new TBar(time.AddMinutes(1), 104, 110, 98, 104, 1000), isNew: false);
|
|
double afterUpdate = apc.Last.Value;
|
|
|
|
Assert.NotEqual(beforeUpdate, afterUpdate);
|
|
}
|
|
|
|
[Fact]
|
|
public void IterativeCorrections_RestoreToOriginalState()
|
|
{
|
|
var apc = new Apchannel(0.2);
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
|
|
|
// Feed 10 new bars
|
|
TBar tenthBar = default;
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
tenthBar = gbm.Next(isNew: true);
|
|
apc.Add(tenthBar, isNew: true);
|
|
}
|
|
|
|
// Remember state after 10 bars
|
|
double stateAfterTen = apc.Last.Value;
|
|
double upperAfterTen = apc.UpperBand;
|
|
double lowerAfterTen = apc.LowerBand;
|
|
|
|
// Generate 9 corrections with isNew=false
|
|
for (int i = 0; i < 9; i++)
|
|
{
|
|
var bar = gbm.Next(isNew: false);
|
|
apc.Add(bar, isNew: false);
|
|
}
|
|
|
|
// Feed the remembered 10th bar again with isNew=false
|
|
var finalResult = apc.Add(tenthBar, isNew: false);
|
|
|
|
// State should match the original state after 10 bars
|
|
Assert.Equal(stateAfterTen, finalResult.Value, Tolerance);
|
|
Assert.Equal(upperAfterTen, apc.UpperBand, Tolerance);
|
|
Assert.Equal(lowerAfterTen, apc.LowerBand, Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void Reset_ClearsState()
|
|
{
|
|
var apc = new Apchannel(0.2);
|
|
var time = DateTime.UtcNow;
|
|
|
|
apc.Add(new TBar(time, 100, 105, 95, 100, 1000));
|
|
apc.Add(new TBar(time.AddMinutes(1), 102, 108, 96, 102, 1000));
|
|
double valueBefore = apc.Last.Value;
|
|
|
|
apc.Reset();
|
|
|
|
Assert.Equal(0, apc.Last.Value);
|
|
Assert.Equal(0, apc.UpperBand);
|
|
Assert.Equal(0, apc.LowerBand);
|
|
Assert.False(apc.IsHot);
|
|
|
|
// After reset, should accept new values
|
|
apc.Add(new TBar(time, 50, 55, 45, 50, 1000));
|
|
Assert.NotEqual(0, apc.Last.Value);
|
|
Assert.NotEqual(valueBefore, apc.Last.Value);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Warmup & Convergence
|
|
|
|
[Fact]
|
|
public void IsHot_BecomesTrueWhenConverged()
|
|
{
|
|
var apc = new Apchannel(0.2);
|
|
var time = DateTime.UtcNow;
|
|
int warmup = apc.WarmupPeriod;
|
|
|
|
Assert.False(apc.IsHot);
|
|
|
|
for (int i = 1; i < warmup; i++)
|
|
{
|
|
apc.Add(new TBar(time.AddMinutes(i), 100, 105, 95, 100, 1000));
|
|
Assert.False(apc.IsHot);
|
|
}
|
|
|
|
apc.Add(new TBar(time.AddMinutes(warmup), 100, 105, 95, 100, 1000));
|
|
Assert.True(apc.IsHot);
|
|
}
|
|
|
|
[Fact]
|
|
public void IsHot_IsAlphaDependent()
|
|
{
|
|
double[] alphas = [0.1, 0.2, 0.5, 0.9];
|
|
int[] expectedSteps = new int[alphas.Length];
|
|
var time = DateTime.UtcNow;
|
|
|
|
for (int i = 0; i < alphas.Length; i++)
|
|
{
|
|
double alpha = alphas[i];
|
|
var apc = new Apchannel(alpha);
|
|
expectedSteps[i] = apc.WarmupPeriod;
|
|
|
|
int steps = 0;
|
|
while (!apc.IsHot && steps < 1000)
|
|
{
|
|
apc.Add(new TBar(time.AddMinutes(steps), 100, 105, 95, 100, 1000));
|
|
steps++;
|
|
}
|
|
}
|
|
|
|
// Warmup times should decrease as alpha increases (faster convergence)
|
|
Assert.True(expectedSteps[0] > expectedSteps[1]);
|
|
Assert.True(expectedSteps[1] > expectedSteps[2]);
|
|
Assert.True(expectedSteps[2] > expectedSteps[3]);
|
|
}
|
|
|
|
[Fact]
|
|
public void WarmupPeriod_IsSetCorrectly()
|
|
{
|
|
var apc1 = new Apchannel(0.1);
|
|
Assert.Equal(30, apc1.WarmupPeriod); // ceil(3.0 / 0.1) = 30
|
|
|
|
var apc2 = new Apchannel(0.2);
|
|
Assert.Equal(15, apc2.WarmupPeriod); // ceil(3.0 / 0.2) = 15
|
|
|
|
var apc3 = new Apchannel(0.5);
|
|
Assert.Equal(6, apc3.WarmupPeriod); // ceil(3.0 / 0.5) = 6
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Robustness (NaN/Infinity)
|
|
|
|
[Fact]
|
|
public void NaN_Input_UsesLastValidValue()
|
|
{
|
|
var apc = new Apchannel(0.2);
|
|
var time = DateTime.UtcNow;
|
|
|
|
apc.Add(new TBar(time, 100, 105, 95, 100, 1000));
|
|
apc.Add(new TBar(time.AddMinutes(1), 102, 108, 96, 102, 1000));
|
|
|
|
var resultAfterNaN = apc.Add(new TBar(time.AddMinutes(2), double.NaN, double.NaN, double.NaN, double.NaN, 1000));
|
|
|
|
Assert.True(double.IsFinite(resultAfterNaN.Value));
|
|
Assert.True(double.IsFinite(apc.UpperBand));
|
|
Assert.True(double.IsFinite(apc.LowerBand));
|
|
Assert.NotEqual(0, resultAfterNaN.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void Infinity_Input_UsesLastValidValue()
|
|
{
|
|
var apc = new Apchannel(0.2);
|
|
var time = DateTime.UtcNow;
|
|
|
|
apc.Add(new TBar(time, 100, 105, 95, 100, 1000));
|
|
apc.Add(new TBar(time.AddMinutes(1), 102, 108, 96, 102, 1000));
|
|
|
|
var resultPosInf = apc.Add(new TBar(time.AddMinutes(2), double.PositiveInfinity,
|
|
double.PositiveInfinity, double.PositiveInfinity,
|
|
double.PositiveInfinity, 1000));
|
|
Assert.True(double.IsFinite(resultPosInf.Value));
|
|
|
|
var resultNegInf = apc.Add(new TBar(time.AddMinutes(3), double.NegativeInfinity,
|
|
double.NegativeInfinity, double.NegativeInfinity,
|
|
double.NegativeInfinity, 1000));
|
|
Assert.True(double.IsFinite(resultNegInf.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void MultipleNaN_ContinuesWithLastValid()
|
|
{
|
|
var apc = new Apchannel(0.2);
|
|
var time = DateTime.UtcNow;
|
|
|
|
apc.Add(new TBar(time, 100, 105, 95, 100, 1000));
|
|
apc.Add(new TBar(time.AddMinutes(1), 102, 108, 96, 102, 1000));
|
|
apc.Add(new TBar(time.AddMinutes(2), 104, 110, 98, 104, 1000));
|
|
|
|
var r1 = apc.Add(new TBar(time.AddMinutes(3), double.NaN, double.NaN, double.NaN, double.NaN, 1000));
|
|
var r2 = apc.Add(new TBar(time.AddMinutes(4), double.NaN, double.NaN, double.NaN, double.NaN, 1000));
|
|
var r3 = apc.Add(new TBar(time.AddMinutes(5), 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));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Span API Tests
|
|
|
|
[Fact]
|
|
public void SpanCalculate_ValidatesInput()
|
|
{
|
|
double[] high = [105, 108, 110, 107, 109];
|
|
double[] low = [95, 96, 98, 97, 99];
|
|
double[] upperBand = new double[5];
|
|
double[] lowerBand = new double[5];
|
|
|
|
// Alpha must be > 0 and <= 1
|
|
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
|
Apchannel.Batch(high, low, upperBand, lowerBand, 0.0));
|
|
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
|
Apchannel.Batch(high, low, upperBand, lowerBand, 1.5));
|
|
|
|
// Arrays must be same length
|
|
double[] wrongSizeLow = new double[3];
|
|
Assert.Throws<ArgumentException>(() =>
|
|
Apchannel.Batch(high, wrongSizeLow, upperBand, lowerBand, 0.2));
|
|
|
|
double[] wrongSizeUpper = new double[3];
|
|
Assert.Throws<ArgumentException>(() =>
|
|
Apchannel.Batch(high, low, wrongSizeUpper, lowerBand, 0.2));
|
|
|
|
double[] wrongSizeLower = new double[3];
|
|
Assert.Throws<ArgumentException>(() =>
|
|
Apchannel.Batch(high, low, upperBand, wrongSizeLower, 0.2));
|
|
}
|
|
|
|
[Fact]
|
|
public void SpanCalculate_MatchesIterativeCalc()
|
|
{
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
|
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
|
|
double[] high = bars.Select(b => b.High).ToArray();
|
|
double[] low = bars.Select(b => b.Low).ToArray();
|
|
double[] upperBandSpan = new double[100];
|
|
double[] lowerBandSpan = new double[100];
|
|
|
|
// Calculate using span
|
|
Apchannel.Batch(high, low, upperBandSpan, lowerBandSpan, 0.2);
|
|
|
|
// Calculate iteratively
|
|
var apc = new Apchannel(0.2);
|
|
double[] upperBandIter = new double[100];
|
|
double[] lowerBandIter = new double[100];
|
|
|
|
for (int i = 0; i < 100; i++)
|
|
{
|
|
apc.Add(bars[i]);
|
|
upperBandIter[i] = apc.UpperBand;
|
|
lowerBandIter[i] = apc.LowerBand;
|
|
}
|
|
|
|
// Compare
|
|
for (int i = 0; i < 100; i++)
|
|
{
|
|
Assert.Equal(upperBandIter[i], upperBandSpan[i], Tolerance);
|
|
Assert.Equal(lowerBandIter[i], lowerBandSpan[i], Tolerance);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void SpanCalculate_HandlesNaN()
|
|
{
|
|
double[] high = [105, double.NaN, 110, 107, double.PositiveInfinity];
|
|
double[] low = [95, 96, double.NaN, 97, double.NegativeInfinity];
|
|
double[] upperBand = new double[5];
|
|
double[] lowerBand = new double[5];
|
|
|
|
Apchannel.Batch(high, low, upperBand, lowerBand, 0.2);
|
|
|
|
foreach (var val in upperBand)
|
|
{
|
|
Assert.True(double.IsFinite(val), $"Expected finite value but got {val}");
|
|
}
|
|
|
|
foreach (var val in lowerBand)
|
|
{
|
|
Assert.True(double.IsFinite(val), $"Expected finite value but got {val}");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void SpanCalculate_ZeroAllocation()
|
|
{
|
|
double[] high = new double[10000];
|
|
double[] low = new double[10000];
|
|
double[] upperBand = new double[10000];
|
|
double[] lowerBand = 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;
|
|
}
|
|
|
|
// Warm up
|
|
Apchannel.Batch(high, low, upperBand, lowerBand, 0.2);
|
|
|
|
// Verify method completes without OOM or stack overflow
|
|
Assert.True(double.IsFinite(upperBand[^1]));
|
|
Assert.True(double.IsFinite(lowerBand[^1]));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Calculate Method Tests
|
|
|
|
[Fact]
|
|
public void Calculate_ReturnsCorrectResultsAndHotIndicator()
|
|
{
|
|
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 42);
|
|
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
|
|
var (results, indicator) = Apchannel.Calculate(bars, 0.2);
|
|
|
|
// Check results
|
|
Assert.Equal(50, results.Count);
|
|
Assert.True(double.IsFinite(results.Last.High));
|
|
Assert.True(double.IsFinite(results.Last.Low));
|
|
|
|
// Check indicator state
|
|
Assert.True(indicator.IsHot);
|
|
Assert.Equal(results.Last.High, indicator.UpperBand, Tolerance);
|
|
Assert.Equal(results.Last.Low, indicator.LowerBand, Tolerance);
|
|
Assert.Equal(15, indicator.WarmupPeriod); // ceil(3.0 / 0.2)
|
|
|
|
// Verify indicator continues correctly
|
|
var nextBar = gbm.Next();
|
|
indicator.Add(nextBar);
|
|
Assert.True(double.IsFinite(indicator.Last.Value));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Chainability Tests
|
|
|
|
[Fact]
|
|
public void Chainability_Works()
|
|
{
|
|
var source = new TBarSeries();
|
|
var apc = new Apchannel(source, 0.2);
|
|
var time = DateTime.UtcNow;
|
|
|
|
var bar = new TBar(time, 100, 105, 95, 100, 1000);
|
|
source.Add(bar);
|
|
|
|
Assert.Equal(105, apc.UpperBand);
|
|
Assert.Equal(95, apc.LowerBand);
|
|
Assert.Equal(100, apc.Last.Value); // (105 + 95) / 2
|
|
}
|
|
|
|
[Fact]
|
|
public void Pub_EventFires()
|
|
{
|
|
var apc = new Apchannel(0.2);
|
|
bool eventFired = false;
|
|
apc.Pub += (object? sender, in TValueEventArgs args) => eventFired = true;
|
|
var time = DateTime.UtcNow;
|
|
|
|
apc.Add(new TBar(time, 100, 105, 95, 100, 1000));
|
|
Assert.True(eventFired);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Indicator-Specific Tests
|
|
|
|
[Fact]
|
|
public void FlatLine_ReturnsSameValue()
|
|
{
|
|
var apc = new Apchannel(0.2);
|
|
var time = DateTime.UtcNow;
|
|
|
|
for (int i = 0; i < 20; i++)
|
|
{
|
|
apc.Add(new TBar(time.AddMinutes(i), 100, 105, 95, 100, 1000));
|
|
}
|
|
|
|
// With flat high/low, bands should converge to input values
|
|
Assert.Equal(105, apc.UpperBand, 1e-6);
|
|
Assert.Equal(95, apc.LowerBand, 1e-6);
|
|
Assert.Equal(100, apc.Last.Value, 1e-6);
|
|
}
|
|
|
|
[Fact]
|
|
public void ChannelWidth_NarrowsWithHighAlpha()
|
|
{
|
|
var apc1 = new Apchannel(0.1); // Slower response
|
|
var apc2 = new Apchannel(0.9); // Faster response
|
|
var time = DateTime.UtcNow;
|
|
|
|
// Feed same data to both
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
double price = 100 + (i % 2 == 0 ? 10 : -10); // Oscillating
|
|
var bar = new TBar(time.AddMinutes(i), price, price + 5, price - 5, price, 1000);
|
|
apc1.Add(bar);
|
|
apc2.Add(bar);
|
|
}
|
|
|
|
double width1 = apc1.UpperBand - apc1.LowerBand;
|
|
double width2 = apc2.UpperBand - apc2.LowerBand;
|
|
|
|
// Higher alpha should track price more closely
|
|
Assert.True(width2 < width1 * 1.5); // Some tolerance for oscillation
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Default Constructor
|
|
|
|
[Fact]
|
|
public void Constructor_DefaultAlpha_UsesPointTwo()
|
|
{
|
|
var apc = new Apchannel(); // default alpha = 0.2
|
|
Assert.Equal(15, apc.WarmupPeriod); // ceil(3.0 / 0.2) = 15
|
|
Assert.Contains("0.20", apc.Name, StringComparison.Ordinal);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Dispose Tests
|
|
|
|
[Fact]
|
|
public void Dispose_UnsubscribesFromSource()
|
|
{
|
|
var source = new TBarSeries();
|
|
var apc = new Apchannel(source, 0.2);
|
|
var time = DateTime.UtcNow;
|
|
|
|
// Verify subscription works
|
|
source.Add(new TBar(time, 100, 105, 95, 100, 1000));
|
|
Assert.NotEqual(0, apc.Last.Value);
|
|
|
|
double valueBeforeDispose = apc.Last.Value;
|
|
|
|
// Dispose should unsubscribe
|
|
apc.Dispose();
|
|
|
|
// Adding to source after dispose should NOT update the indicator
|
|
source.Add(new TBar(time.AddMinutes(1), 200, 210, 190, 200, 5000));
|
|
Assert.Equal(valueBeforeDispose, apc.Last.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void Dispose_DoubleDispose_DoesNotThrow()
|
|
{
|
|
var source = new TBarSeries();
|
|
var apc = new Apchannel(source, 0.2);
|
|
|
|
apc.Dispose();
|
|
var ex = Record.Exception(() => apc.Dispose());
|
|
Assert.Null(ex);
|
|
}
|
|
|
|
[Fact]
|
|
public void Dispose_WithoutSource_DoesNotThrow()
|
|
{
|
|
var apc = new Apchannel(0.2);
|
|
var ex = Record.Exception(() => apc.Dispose());
|
|
Assert.Null(ex);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Update(TBarSeries) Tests
|
|
|
|
[Fact]
|
|
public void UpdateTBarSeries_ReturnsTSeries()
|
|
{
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
|
var bars = gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
|
|
var apc = new Apchannel(0.2);
|
|
var results = apc.Update(bars);
|
|
|
|
Assert.Equal(30, results.Count);
|
|
Assert.True(apc.IsHot); // 30 > WarmupPeriod(15)
|
|
|
|
// Verify all results are finite
|
|
for (int i = 0; i < results.Count; i++)
|
|
{
|
|
Assert.True(double.IsFinite(results[i].Value));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void UpdateTBarSeries_MatchesIterative()
|
|
{
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
|
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
|
|
// Batch via Update(TBarSeries)
|
|
var apcBatch = new Apchannel(0.3);
|
|
var batchResults = apcBatch.Update(bars);
|
|
|
|
// Iterative — fresh instance so both start from same state
|
|
var apcIter = new Apchannel(0.3);
|
|
for (int i = 0; i < bars.Count; i++)
|
|
{
|
|
var val = apcIter.Add(bars[i]);
|
|
Assert.Equal(val.Value, batchResults[i].Value, Tolerance);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void UpdateTBarSeries_EmptySource_ReturnsEmpty()
|
|
{
|
|
var apc = new Apchannel(0.2);
|
|
var emptyBars = new TBarSeries();
|
|
|
|
var results = apc.Update(emptyBars);
|
|
|
|
Assert.Empty(results);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Update(TValue) Tests
|
|
|
|
[Fact]
|
|
public void UpdateTValue_UsesValueForBothHighAndLow()
|
|
{
|
|
var apc = new Apchannel(0.5);
|
|
var time = DateTime.UtcNow;
|
|
|
|
// When using TValue, value is used for both high and low
|
|
var result = apc.Update(new TValue(time.Ticks, 100.0));
|
|
|
|
// Upper and lower bands should equal the value (first bar)
|
|
Assert.Equal(100.0, apc.UpperBand, Tolerance);
|
|
Assert.Equal(100.0, apc.LowerBand, Tolerance);
|
|
Assert.Equal(100.0, result.Value, Tolerance); // mid = (100+100)/2
|
|
|
|
// Second value
|
|
var result2 = apc.Update(new TValue(time.AddMinutes(1).Ticks, 110.0));
|
|
|
|
// EMA: 0.5 * 100 + 0.5 * 110 = 105
|
|
Assert.Equal(105.0, apc.UpperBand, Tolerance);
|
|
Assert.Equal(105.0, apc.LowerBand, Tolerance);
|
|
Assert.Equal(105.0, result2.Value, Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void UpdateTValue_IsNew_False_RestoresState()
|
|
{
|
|
var apc = new Apchannel(0.2);
|
|
var time = DateTime.UtcNow;
|
|
|
|
apc.Update(new TValue(time.Ticks, 100.0), isNew: true);
|
|
double valueAfterOne = apc.Last.Value;
|
|
|
|
apc.Update(new TValue(time.AddMinutes(1).Ticks, 110.0), isNew: true);
|
|
double valueAfterTwo = apc.Last.Value;
|
|
Assert.NotEqual(valueAfterOne, valueAfterTwo);
|
|
|
|
// Correction with same value restores prior state then reapplies
|
|
apc.Update(new TValue(time.AddMinutes(1).Ticks, 110.0), isNew: false);
|
|
double valueAfterSameCorrection = apc.Last.Value;
|
|
|
|
// Correction with different value produces different result
|
|
apc.Update(new TValue(time.AddMinutes(1).Ticks, 120.0), isNew: false);
|
|
double valueAfterDifferent = apc.Last.Value;
|
|
|
|
Assert.NotEqual(valueAfterSameCorrection, valueAfterDifferent);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Update(TSeries) Tests
|
|
|
|
[Fact]
|
|
public void UpdateTSeries_ReturnsTSeries()
|
|
{
|
|
var times = new List<long>();
|
|
var values = new List<double>();
|
|
var time = DateTime.UtcNow;
|
|
|
|
for (int i = 0; i < 30; i++)
|
|
{
|
|
times.Add(time.AddMinutes(i).Ticks);
|
|
values.Add(100.0 + i);
|
|
}
|
|
|
|
var source = new TSeries(times, values);
|
|
var apc = new Apchannel(0.2);
|
|
var results = apc.Update(source);
|
|
|
|
Assert.Equal(30, results.Count);
|
|
Assert.True(apc.IsHot);
|
|
|
|
for (int i = 0; i < results.Count; i++)
|
|
{
|
|
Assert.True(double.IsFinite(results[i].Value));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void UpdateTSeries_EmptySource_ReturnsEmpty()
|
|
{
|
|
var apc = new Apchannel(0.2);
|
|
var emptySource = new TSeries([], []);
|
|
|
|
var results = apc.Update(emptySource);
|
|
|
|
Assert.Empty(results);
|
|
}
|
|
|
|
[Fact]
|
|
public void UpdateTSeries_MatchesUpdateTValue()
|
|
{
|
|
var times = new List<long>();
|
|
var values = new List<double>();
|
|
var time = DateTime.UtcNow;
|
|
|
|
for (int i = 0; i < 20; i++)
|
|
{
|
|
times.Add(time.AddMinutes(i).Ticks);
|
|
values.Add(100.0 + (i * 2.5));
|
|
}
|
|
|
|
var source = new TSeries(times, values);
|
|
|
|
// Batch via Update(TSeries)
|
|
var apcBatch = new Apchannel(0.3);
|
|
var batchResults = apcBatch.Update(source);
|
|
|
|
// Iterative via Update(TValue)
|
|
var apcIter = new Apchannel(0.3);
|
|
for (int i = 0; i < source.Count; i++)
|
|
{
|
|
var val = apcIter.Update(source[i], isNew: true);
|
|
Assert.Equal(val.Value, batchResults[i].Value, Tolerance);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Prime Tests
|
|
|
|
[Fact]
|
|
public void Prime_SetsIndicatorToHot()
|
|
{
|
|
var apc = new Apchannel(0.2); // WarmupPeriod = 15
|
|
|
|
double[] data = new double[20];
|
|
for (int i = 0; i < data.Length; i++)
|
|
{
|
|
data[i] = 100.0 + i;
|
|
}
|
|
|
|
Assert.False(apc.IsHot);
|
|
|
|
apc.Prime(data);
|
|
|
|
Assert.True(apc.IsHot);
|
|
Assert.True(double.IsFinite(apc.Last.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Prime_EmptySpan_DoesNotThrow()
|
|
{
|
|
var apc = new Apchannel(0.2);
|
|
var ex = Record.Exception(() => apc.Prime(ReadOnlySpan<double>.Empty));
|
|
Assert.Null(ex);
|
|
Assert.False(apc.IsHot);
|
|
}
|
|
|
|
[Fact]
|
|
public void Prime_ResetsBeforeProcessing()
|
|
{
|
|
var apc = new Apchannel(0.5);
|
|
var time = DateTime.UtcNow;
|
|
|
|
// Feed some bars first
|
|
apc.Add(new TBar(time, 200, 210, 190, 200, 1000));
|
|
apc.Add(new TBar(time.AddMinutes(1), 205, 215, 195, 205, 1000));
|
|
|
|
double[] primeData = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109];
|
|
apc.Prime(primeData);
|
|
|
|
// After Prime, upper=lower (since TValue uses same value for both)
|
|
// and bands should track primeData, not the old bars
|
|
Assert.Equal(apc.UpperBand, apc.LowerBand, Tolerance);
|
|
Assert.True(apc.Last.Value < 150); // Should be near primeData values, not 200
|
|
}
|
|
|
|
[Fact]
|
|
public void Prime_WithStep_UsesCorrectTimeSpacing()
|
|
{
|
|
var apc = new Apchannel(0.2);
|
|
double[] data = [100, 102, 104, 106, 108];
|
|
var step = TimeSpan.FromHours(1);
|
|
|
|
apc.Prime(data, step);
|
|
|
|
Assert.True(double.IsFinite(apc.Last.Value));
|
|
// Verify that the time in Last reflects the step spacing
|
|
Assert.True(apc.Last.Time > 0);
|
|
}
|
|
|
|
[Fact]
|
|
public void Prime_ThenUpdate_ContinuesCorrectly()
|
|
{
|
|
var apc = new Apchannel(0.2);
|
|
|
|
// Prime with 20 values to reach IsHot
|
|
double[] primeData = new double[20];
|
|
for (int i = 0; i < primeData.Length; i++)
|
|
{
|
|
primeData[i] = 100.0 + i;
|
|
}
|
|
|
|
apc.Prime(primeData);
|
|
Assert.True(apc.IsHot);
|
|
|
|
double valueAfterPrime = apc.Last.Value;
|
|
|
|
// Continue with Update — should build on primed state
|
|
var time = DateTime.UtcNow;
|
|
apc.Add(new TBar(time, 125, 130, 120, 125, 1000));
|
|
|
|
Assert.NotEqual(valueAfterPrime, apc.Last.Value);
|
|
Assert.True(double.IsFinite(apc.Last.Value));
|
|
Assert.True(double.IsFinite(apc.UpperBand));
|
|
Assert.True(double.IsFinite(apc.LowerBand));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Batch(Span) Edge Cases
|
|
|
|
[Fact]
|
|
public void SpanBatch_EmptyArrays_DoesNotThrow()
|
|
{
|
|
double[] high = [];
|
|
double[] low = [];
|
|
double[] upper = [];
|
|
double[] lower = [];
|
|
|
|
var ex = Record.Exception(() => Apchannel.Batch(high, low, upper, lower, 0.2));
|
|
Assert.Null(ex);
|
|
}
|
|
|
|
[Fact]
|
|
public void SpanBatch_SingleElement_ReturnsInputValues()
|
|
{
|
|
double[] high = [110];
|
|
double[] low = [90];
|
|
double[] upper = new double[1];
|
|
double[] lower = new double[1];
|
|
|
|
Apchannel.Batch(high, low, upper, lower, 0.2);
|
|
|
|
Assert.Equal(110, upper[0], Tolerance);
|
|
Assert.Equal(90, lower[0], Tolerance);
|
|
}
|
|
|
|
#endregion
|
|
}
|