Files
Encelado/Encelado/tests/Encelado.Tests/IndicatorTests.cs
T

278 lines
6.3 KiB
C#

using Encelado.Core.Indicators;
using Encelado.Core.Market;
namespace Encelado.Tests;
public class RollingWindowTests
{
[Fact]
public void IndexerIsMostRecentFirst()
{
RollingWindow<int> window = new(3);
window.Add(1);
window.Add(2);
window.Add(3);
Assert.True(window.IsFull);
Assert.Equal(3, window[0]);
Assert.Equal(2, window[1]);
Assert.Equal(1, window[2]);
Assert.Equal(3, window.Newest);
Assert.Equal(1, window.Oldest);
}
[Fact]
public void EvictsOldestOnceFull()
{
RollingWindow<int> window = new(2);
Assert.False(window.TryAdd(1, out _));
Assert.False(window.TryAdd(2, out _));
Assert.True(window.TryAdd(3, out int evicted));
Assert.Equal(1, evicted);
Assert.Equal(3, window[0]);
Assert.Equal(2, window[1]);
}
[Fact]
public void ThrowsWhenIndexingBeyondPopulatedRange()
{
RollingWindow<int> window = new(4);
window.Add(7);
Assert.Equal(7, window[0]);
Assert.Throws<ArgumentOutOfRangeException>(() => window[1]);
}
}
public class MovingAverageTests
{
[Fact]
public void SmaAveragesTheWindowOnly()
{
Sma sma = new(3);
sma.Update(1);
sma.Update(2);
Assert.False(sma.IsReady);
Assert.Equal(2, sma.Update(3), 10);
Assert.True(sma.IsReady);
// The 1 drops out: (2 + 3 + 4) / 3.
Assert.Equal(3, sma.Update(4), 10);
}
[Fact]
public void EmaSeedsWithSmaThenSmooths()
{
Ema ema = new(3);
ema.Update(1);
ema.Update(2);
Assert.False(ema.IsReady);
Assert.Equal(2, ema.Update(3), 10);
Assert.True(ema.IsReady);
// alpha = 2/(3+1) = 0.5
Assert.Equal(3, ema.Update(4), 10);
Assert.Equal(4, ema.Update(5), 10);
}
[Fact]
public void ResetClearsState()
{
Ema ema = new(2);
ema.Update(10);
ema.Update(20);
Assert.True(ema.IsReady);
ema.Reset();
Assert.False(ema.IsReady);
Assert.True(double.IsNaN(ema.Value));
}
}
public class RsiTests
{
[Fact]
public void MonotonicRiseSaturatesAtOneHundred()
{
Rsi rsi = new(14);
for (int i = 1; i <= 40; i++)
{
rsi.Update(i);
}
Assert.True(rsi.IsReady);
Assert.Equal(100, rsi.Value, 6);
}
[Fact]
public void MonotonicFallSaturatesAtZero()
{
Rsi rsi = new(14);
for (int i = 40; i >= 1; i--)
{
rsi.Update(i);
}
Assert.True(rsi.IsReady);
Assert.Equal(0, rsi.Value, 6);
}
[Fact]
public void FlatSeriesIsNeutral()
{
Rsi rsi = new(14);
for (int i = 0; i < 40; i++)
{
rsi.Update(100);
}
Assert.Equal(50, rsi.Value, 6);
}
[Fact]
public void NeedsPeriodPlusOneSamples()
{
Rsi rsi = new(5);
for (int i = 0; i < 5; i++)
{
rsi.Update(100 + i);
Assert.False(rsi.IsReady);
}
rsi.Update(105);
Assert.True(rsi.IsReady);
}
}
public class AtrTests
{
[Fact]
public void ConstantRangeConvergesToThatRange()
{
Atr atr = new(14);
for (int i = 0; i < 30; i++)
{
atr.Update(new Bar(DateTime.UtcNow, 100, 101, 99, 100, 1000, 100, 10));
}
Assert.True(atr.IsReady);
Assert.Equal(2, atr.Value, 6);
}
[Fact]
public void GapsCountTowardsTrueRange()
{
Atr atr = new(2);
atr.Update(new Bar(DateTime.UtcNow, 100, 100.5, 99.5, 100, 1, 100, 1));
// Gaps up to 110: true range is 110.5 - 100 = 10.5, not the 1.0 intraday range.
atr.Update(new Bar(DateTime.UtcNow, 110, 110.5, 109.5, 110, 1, 110, 1));
Assert.True(atr.IsReady);
Assert.Equal((1.0 + 10.5) / 2, atr.Value, 6);
}
}
public class DonchianTests
{
[Fact]
public void TracksRollingExtremes()
{
Donchian channel = new(3);
channel.Update(10, 5);
channel.Update(12, 6);
channel.Update(11, 4);
Assert.True(channel.IsReady);
Assert.Equal(12, channel.Upper);
Assert.Equal(4, channel.Lower);
}
[Fact]
public void RescansWhenTheExtremeFallsOutOfTheWindow()
{
Donchian channel = new(3);
channel.Update(20, 1);
channel.Update(12, 6);
channel.Update(11, 4);
Assert.Equal(20, channel.Upper);
Assert.Equal(1, channel.Lower);
// The 20/1 bar rolls out; extremes must be recomputed from the survivors.
channel.Update(13, 7);
Assert.Equal(13, channel.Upper);
Assert.Equal(4, channel.Lower);
}
}
public class BollingerTests
{
[Fact]
public void ConstantSeriesCollapsesTheBands()
{
BollingerBands bands = new(5, 2.0);
for (int i = 0; i < 10; i++)
{
bands.Update(50);
}
Assert.True(bands.IsReady);
Assert.Equal(50, bands.Value, 10);
Assert.Equal(50, bands.Upper, 10);
Assert.Equal(50, bands.Lower, 10);
}
[Fact]
public void BandsSurroundTheMean()
{
BollingerBands bands = new(4, 2.0);
double[] samples = [10, 12, 14, 16];
foreach (double s in samples)
{
bands.Update(s);
}
Assert.Equal(13, bands.Value, 10);
Assert.True(bands.Upper > bands.Value);
Assert.True(bands.Lower < bands.Value);
Assert.InRange(bands.PercentB(bands.Upper), 0.99, 1.01);
}
}
public class StdDevTests
{
[Fact]
public void MatchesTheSampleStandardDeviation()
{
RollingStdDev sd = new(4);
foreach (double s in new double[] { 2, 4, 4, 6 })
{
sd.Update(s);
}
// mean 4; sample variance = (4 + 0 + 0 + 4) / 3
Assert.Equal(Math.Sqrt(8.0 / 3.0), sd.Value, 10);
}
}
public class SessionVwapTests
{
[Fact]
public void WeightsPricesByVolume()
{
SessionVwap vwap = new();
vwap.Update(new Bar(DateTime.UtcNow, 10, 10, 10, 10, 100, 10, 1));
vwap.Update(new Bar(DateTime.UtcNow, 20, 20, 20, 20, 300, 20, 1));
// (10*100 + 20*300) / 400
Assert.Equal(17.5, vwap.Value, 10);
vwap.Reset();
Assert.False(vwap.IsReady);
}
}