using Encelado.Core.Indicators; using Encelado.Core.Market; namespace Encelado.Tests; public class EfficiencyRatioTests { [Fact] public void AStraightLineIsPerfectlyEfficient() { EfficiencyRatio er = new(10); for (int i = 1; i <= 20; i++) { er.Update(i); } Assert.True(er.IsReady); Assert.Equal(1.0, er.Value, 10); } [Fact] public void PureOscillationIsCompletelyInefficient() { EfficiencyRatio er = new(10); for (int i = 0; i < 30; i++) { er.Update(i % 2 == 0 ? 100 : 101); } Assert.True(er.IsReady); Assert.Equal(0.0, er.Value, 10); } [Fact] public void ANoisyTrendSitsBetweenTheExtremes() { EfficiencyRatio er = new(10); double price = 100; for (int i = 0; i < 40; i++) { price += i % 3 == 0 ? -0.5 : 1.0; er.Update(price); } Assert.InRange(er.Value, 0.05, 0.95); } [Fact] public void NeedsAFullWindowBeforeReporting() { EfficiencyRatio er = new(5); for (int i = 0; i < 5; i++) { er.Update(100 + i); Assert.False(er.IsReady); } er.Update(105); Assert.True(er.IsReady); } } public class RealizedVolatilityTests { [Fact] public void AFlatSeriesHasZeroVolatility() { RealizedVolatility vol = new(20, 525_600); for (int i = 0; i < 40; i++) { vol.Update(100); } Assert.True(vol.IsReady); Assert.Equal(0, vol.Value, 10); } [Fact] public void AnnualisationScalesByTheSquareRootOfBarsPerYear() { RealizedVolatility perBar = new(20, 1); RealizedVolatility annual = new(20, 4); double price = 100; for (int i = 0; i < 40; i++) { price *= i % 2 == 0 ? 1.01 : 0.995; perBar.Update(price); annual.Update(price); } Assert.True(perBar.Value > 0); Assert.Equal(perBar.Value * 2, annual.Value, 10); Assert.Equal(perBar.PerBar, annual.PerBar, 12); } [Fact] public void IgnoresNonPositivePrices() { RealizedVolatility vol = new(5, 1); for (int i = 0; i < 10; i++) { vol.Update(100); } double before = vol.Value; vol.Update(0); vol.Update(-5); Assert.Equal(before, vol.Value, 12); } } public class KeltnerTests { [Fact] public void ChannelSitsAtTheAtrMultipleAroundTheEma() { Keltner keltner = new(period: 10, atrMultiplier: 2.0, atrPeriod: 10); for (int i = 0; i < 40; i++) { keltner.Update(new Bar(DateTime.UtcNow, 100, 100.5, 99.5, 100, 1000, 100, 10)); } Assert.True(keltner.IsReady); Assert.Equal(100, keltner.Value, 6); // Constant 1.0 range means ATR = 1, so the channel is +/- 2. Assert.Equal(102, keltner.Upper, 6); Assert.Equal(98, keltner.Lower, 6); Assert.Equal(4, keltner.Width, 6); } } public class RollingZScoreTests { [Fact] public void ScoresTheLatestSampleAgainstItsOwnWindow() { RollingZScore z = new(4); foreach (double v in new double[] { 2, 4, 4, 6 }) { z.Update(v); } // mean 4, sample stddev sqrt(8/3); the last sample sits 2 above the mean. Assert.True(z.IsReady); Assert.Equal(2.0 / Math.Sqrt(8.0 / 3.0), z.Value, 10); Assert.Equal(4, z.Mean, 10); } [Fact] public void AConstantSeriesScoresZeroInsteadOfDividingByZero() { RollingZScore z = new(5); for (int i = 0; i < 10; i++) { z.Update(42); } Assert.Equal(0, z.Value, 10); } } public class BollingerDispersionTests { [Fact] public void ExposesTheStandardDeviationTheBandsAreBuiltFrom() { BollingerBands bands = new(4, 2.0); foreach (double v in new double[] { 2, 4, 4, 6 }) { bands.Update(v); } double sd = Math.Sqrt(8.0 / 3.0); Assert.Equal(sd, bands.StandardDeviation, 10); Assert.Equal(4 + (2 * sd), bands.Upper, 10); Assert.Equal(4 - (2 * sd), bands.Lower, 10); } }