278 lines
6.3 KiB
C#
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);
|
|
}
|
|
}
|