mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-24 05:28:05 +00:00
Refactor documentation for various filters and indicators to enhance clarity and consistency
- Updated Bessel, Bilateral, Blma, Butter, Conv, Ema, Kama, LSMA, MAMA, MGDI, SSF, USF, ATR, ADL, and ADOSC documentation to use bullet points for key concepts and features. - Added a new Qodana configuration file for code analysis. - Removed coverage configuration from Quantower.Tests.csproj to streamline testing setup.
This commit is contained in:
@@ -0,0 +1,675 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class AbberTests
|
||||
{
|
||||
[Fact]
|
||||
public void Abber_Constructor_ValidatesInput()
|
||||
{
|
||||
// Period validation
|
||||
Assert.Throws<ArgumentException>(() => new Abber(0));
|
||||
Assert.Throws<ArgumentException>(() => new Abber(-1));
|
||||
|
||||
// Multiplier validation
|
||||
Assert.Throws<ArgumentException>(() => new Abber(10, 0));
|
||||
Assert.Throws<ArgumentException>(() => new Abber(10, -1));
|
||||
|
||||
// Valid construction
|
||||
var abber = new Abber(10);
|
||||
Assert.NotNull(abber);
|
||||
|
||||
var abber2 = new Abber(20, 3.0);
|
||||
Assert.NotNull(abber2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_Calc_ReturnsValue()
|
||||
{
|
||||
var abber = new Abber(10);
|
||||
|
||||
Assert.Equal(0, abber.Last.Value);
|
||||
Assert.Equal(0, abber.Upper.Value);
|
||||
Assert.Equal(0, abber.Lower.Value);
|
||||
|
||||
TValue result = abber.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.Equal(result.Value, abber.Last.Value);
|
||||
Assert.True(double.IsFinite(abber.Upper.Value));
|
||||
Assert.True(double.IsFinite(abber.Lower.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_FirstValue_ReturnsExpected()
|
||||
{
|
||||
var abber = new Abber(10);
|
||||
|
||||
// First value: source = 100
|
||||
// SMA(1) = 100, Deviation = |100 - 100| = 0, AvgDeviation = 0
|
||||
// Middle = 100, Upper = 100 + 0 = 100, Lower = 100 - 0 = 100
|
||||
abber.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
Assert.Equal(100.0, abber.Last.Value, 1e-10);
|
||||
Assert.Equal(100.0, abber.Upper.Value, 1e-10);
|
||||
Assert.Equal(100.0, abber.Lower.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var abber = new Abber(10);
|
||||
|
||||
abber.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
|
||||
double value1 = abber.Last.Value;
|
||||
|
||||
abber.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
|
||||
double value2 = abber.Last.Value;
|
||||
|
||||
// Values should change with new data
|
||||
Assert.NotEqual(value1, value2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_Calc_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var abber = new Abber(10);
|
||||
|
||||
abber.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
|
||||
abber.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
|
||||
double beforeUpdate = abber.Last.Value;
|
||||
|
||||
abber.Update(new TValue(DateTime.UtcNow, 120), isNew: false);
|
||||
double afterUpdate = abber.Last.Value;
|
||||
|
||||
// Update should change the value
|
||||
Assert.NotEqual(beforeUpdate, afterUpdate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_Reset_ClearsState()
|
||||
{
|
||||
var abber = new Abber(10);
|
||||
|
||||
abber.Update(new TValue(DateTime.UtcNow, 100));
|
||||
abber.Update(new TValue(DateTime.UtcNow, 105));
|
||||
double middleBefore = abber.Last.Value;
|
||||
|
||||
abber.Reset();
|
||||
|
||||
Assert.Equal(0, abber.Last.Value);
|
||||
Assert.Equal(0, abber.Upper.Value);
|
||||
Assert.Equal(0, abber.Lower.Value);
|
||||
Assert.False(abber.IsHot);
|
||||
|
||||
// After reset, should accept new values
|
||||
abber.Update(new TValue(DateTime.UtcNow, 50));
|
||||
Assert.NotEqual(0, abber.Last.Value);
|
||||
Assert.NotEqual(middleBefore, abber.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_Properties_Accessible()
|
||||
{
|
||||
var abber = new Abber(10, 2.5);
|
||||
|
||||
Assert.Equal(0, abber.Last.Value);
|
||||
Assert.False(abber.IsHot);
|
||||
Assert.Contains("Abber", abber.Name, StringComparison.Ordinal);
|
||||
Assert.Equal(10, abber.WarmupPeriod);
|
||||
|
||||
abber.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
Assert.NotEqual(0, abber.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_IsHot_BecomesTrueWhenBufferFull()
|
||||
{
|
||||
var abber = new Abber(5);
|
||||
|
||||
Assert.False(abber.IsHot);
|
||||
|
||||
for (int i = 1; i <= 4; i++)
|
||||
{
|
||||
abber.Update(new TValue(DateTime.UtcNow, 100 + i));
|
||||
Assert.False(abber.IsHot);
|
||||
}
|
||||
|
||||
abber.Update(new TValue(DateTime.UtcNow, 105));
|
||||
Assert.True(abber.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_CalculatesCorrectBands()
|
||||
{
|
||||
var abber = new Abber(3, 2.0);
|
||||
|
||||
// Bar 1: source = 100
|
||||
// SMA = 100, Deviation = 0, AvgDev = 0
|
||||
abber.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.Equal(100.0, abber.Last.Value, 1e-10);
|
||||
|
||||
// Bar 2: source = 110
|
||||
// SMA(2) = (100+110)/2 = 105
|
||||
// Dev1 = |100 - 100| = 0 (calculated when 100 was added, SMA was 100)
|
||||
// Dev2 = |110 - 100| = 10 (calculated when 110 is added, SMA was 100)
|
||||
// AvgDev = (0+10)/2 = 5
|
||||
// Upper = 105 + 2*5 = 115, Lower = 105 - 2*5 = 95
|
||||
abber.Update(new TValue(DateTime.UtcNow, 110));
|
||||
Assert.Equal(105.0, abber.Last.Value, 1e-10);
|
||||
|
||||
// Bar 3: source = 120
|
||||
// SMA(3) = (100+110+120)/3 = 110
|
||||
// Dev3 = |120 - 105| = 15 (calculated when 120 is added, SMA was 105)
|
||||
// AvgDev = (0+10+15)/3 = 8.333...
|
||||
// Upper = 110 + 2*8.333 = 126.666..., Lower = 110 - 2*8.333 = 93.333...
|
||||
abber.Update(new TValue(DateTime.UtcNow, 120));
|
||||
Assert.Equal(110.0, abber.Last.Value, 1e-10);
|
||||
Assert.Equal(110.0 + 2.0 * 25.0 / 3.0, abber.Upper.Value, 1e-10);
|
||||
Assert.Equal(110.0 - 2.0 * 25.0 / 3.0, abber.Lower.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_SlidingWindow_Works()
|
||||
{
|
||||
var abber = new Abber(3, 2.0);
|
||||
|
||||
// Feed initial values
|
||||
abber.Update(new TValue(DateTime.UtcNow, 100));
|
||||
abber.Update(new TValue(DateTime.UtcNow, 110));
|
||||
abber.Update(new TValue(DateTime.UtcNow, 120));
|
||||
|
||||
double middle1 = abber.Last.Value;
|
||||
|
||||
// Add another value - window slides
|
||||
abber.Update(new TValue(DateTime.UtcNow, 130));
|
||||
|
||||
// SMA(3) should now be (110+120+130)/3 = 120
|
||||
Assert.NotEqual(middle1, abber.Last.Value);
|
||||
Assert.Equal(120.0, abber.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var abber = new Abber(5);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
|
||||
// Feed 10 new values
|
||||
TValue tenthInput = default;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
tenthInput = new TValue(bar.Time, bar.Close);
|
||||
abber.Update(tenthInput, isNew: true);
|
||||
}
|
||||
|
||||
// Remember state after 10 values
|
||||
double middleAfterTen = abber.Last.Value;
|
||||
double upperAfterTen = abber.Upper.Value;
|
||||
double lowerAfterTen = abber.Lower.Value;
|
||||
|
||||
// Generate 9 corrections with isNew=false (different values)
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: false);
|
||||
abber.Update(new TValue(bar.Time, bar.Close), isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered 10th input again with isNew=false
|
||||
abber.Update(tenthInput, isNew: false);
|
||||
|
||||
// State should match the original state after 10 values
|
||||
Assert.Equal(middleAfterTen, abber.Last.Value, 1e-10);
|
||||
Assert.Equal(upperAfterTen, abber.Upper.Value, 1e-10);
|
||||
Assert.Equal(lowerAfterTen, abber.Lower.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_BatchCalc_MatchesIterativeCalc()
|
||||
{
|
||||
var abberIterative = new Abber(10);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
|
||||
// Generate data
|
||||
var series = new TSeries();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
Assert.True(series.Count > 0);
|
||||
|
||||
// Calculate iteratively
|
||||
var iterativeMiddle = new List<double>();
|
||||
var iterativeUpper = new List<double>();
|
||||
var iterativeLower = new List<double>();
|
||||
foreach (var item in series)
|
||||
{
|
||||
abberIterative.Update(item);
|
||||
iterativeMiddle.Add(abberIterative.Last.Value);
|
||||
iterativeUpper.Add(abberIterative.Upper.Value);
|
||||
iterativeLower.Add(abberIterative.Lower.Value);
|
||||
}
|
||||
|
||||
// Calculate batch
|
||||
var abberBatch = new Abber(10);
|
||||
var (batchMiddle, batchUpper, batchLower) = abberBatch.Update(series);
|
||||
|
||||
// Compare
|
||||
Assert.Equal(iterativeMiddle.Count, batchMiddle.Count);
|
||||
for (int i = 0; i < iterativeMiddle.Count; i++)
|
||||
{
|
||||
Assert.Equal(iterativeMiddle[i], batchMiddle[i].Value, 1e-10);
|
||||
Assert.Equal(iterativeUpper[i], batchUpper[i].Value, 1e-10);
|
||||
Assert.Equal(iterativeLower[i], batchLower[i].Value, 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var abber = new Abber(5);
|
||||
|
||||
// Feed some valid values
|
||||
abber.Update(new TValue(DateTime.UtcNow, 100));
|
||||
abber.Update(new TValue(DateTime.UtcNow, 105));
|
||||
|
||||
// Feed NaN - should use last valid value
|
||||
var resultAfterNaN = abber.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
// Result should be finite (not NaN)
|
||||
Assert.True(double.IsFinite(resultAfterNaN.Value));
|
||||
Assert.True(double.IsFinite(abber.Upper.Value));
|
||||
Assert.True(double.IsFinite(abber.Lower.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var abber = new Abber(5);
|
||||
|
||||
// Feed some valid values
|
||||
abber.Update(new TValue(DateTime.UtcNow, 100));
|
||||
abber.Update(new TValue(DateTime.UtcNow, 105));
|
||||
|
||||
// Feed positive infinity
|
||||
var resultAfterPosInf = abber.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(resultAfterPosInf.Value));
|
||||
Assert.True(double.IsFinite(abber.Upper.Value));
|
||||
Assert.True(double.IsFinite(abber.Lower.Value));
|
||||
|
||||
// Feed negative infinity
|
||||
var resultAfterNegInf = abber.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
|
||||
Assert.True(double.IsFinite(resultAfterNegInf.Value));
|
||||
Assert.True(double.IsFinite(abber.Upper.Value));
|
||||
Assert.True(double.IsFinite(abber.Lower.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_MultipleNaN_ContinuesWithLastValid()
|
||||
{
|
||||
var abber = new Abber(5);
|
||||
|
||||
// Feed valid values
|
||||
abber.Update(new TValue(DateTime.UtcNow, 100));
|
||||
abber.Update(new TValue(DateTime.UtcNow, 105));
|
||||
abber.Update(new TValue(DateTime.UtcNow, 110));
|
||||
|
||||
// Feed multiple NaN values
|
||||
var r1 = abber.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
var r2 = abber.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
var r3 = abber.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
// All results should be finite
|
||||
Assert.True(double.IsFinite(r1.Value));
|
||||
Assert.True(double.IsFinite(r2.Value));
|
||||
Assert.True(double.IsFinite(r3.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_StaticBatch_Works()
|
||||
{
|
||||
var series = new TSeries();
|
||||
series.Add(DateTime.UtcNow, 100);
|
||||
series.Add(DateTime.UtcNow, 110);
|
||||
series.Add(DateTime.UtcNow, 120);
|
||||
series.Add(DateTime.UtcNow, 130);
|
||||
series.Add(DateTime.UtcNow, 140);
|
||||
|
||||
var (middle, upper, lower) = Abber.Batch(series, 3);
|
||||
|
||||
Assert.Equal(5, middle.Count);
|
||||
Assert.Equal(5, upper.Count);
|
||||
Assert.Equal(5, lower.Count);
|
||||
|
||||
// All values should be finite
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(middle[i].Value));
|
||||
Assert.True(double.IsFinite(upper[i].Value));
|
||||
Assert.True(double.IsFinite(lower[i].Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_Period1_ReturnsDirectCalculation()
|
||||
{
|
||||
var abber = new Abber(1);
|
||||
|
||||
// Single value: SMA(1) = 100, Deviation = 0
|
||||
abber.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.Equal(100.0, abber.Last.Value, 1e-10);
|
||||
Assert.Equal(100.0, abber.Upper.Value, 1e-10);
|
||||
Assert.Equal(100.0, abber.Lower.Value, 1e-10);
|
||||
|
||||
// Next value: SMA(1) = 110, Deviation from previous SMA = |110 - 100| = 10
|
||||
// But with period 1, the old value drops out, so AvgDev = |110 - 110| = 0?
|
||||
// Actually deviation is calculated BEFORE adding to buffer
|
||||
// When 110 comes in, SMA is still 100, so Dev = |110 - 100| = 10
|
||||
// Then buffer updates to just [110], so SMA = 110, AvgDev = 10
|
||||
abber.Update(new TValue(DateTime.UtcNow, 110));
|
||||
Assert.Equal(110.0, abber.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
// ============== Span API Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void Abber_SpanBatch_ValidatesInput()
|
||||
{
|
||||
double[] source = [100, 110, 120];
|
||||
double[] middle = new double[3];
|
||||
double[] upper = new double[3];
|
||||
double[] lower = new double[3];
|
||||
|
||||
// Period must be > 0
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Abber.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 0));
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Abber.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), -1));
|
||||
|
||||
// Multiplier must be > 0
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Abber.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 3, 0));
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Abber.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 3, -1));
|
||||
|
||||
// Output buffers must be same length as input
|
||||
double[] shortOutput = new double[2];
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Abber.Batch(source.AsSpan(), shortOutput.AsSpan(), upper.AsSpan(), lower.AsSpan(), 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_SpanBatch_MatchesTSeriesBatch()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var series = new TSeries();
|
||||
|
||||
double[] source = new double[100];
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
series.Add(bar.Time, bar.Close);
|
||||
source[i] = bar.Close;
|
||||
}
|
||||
|
||||
// Calculate with TSeries API
|
||||
var (tseriesMiddle, tseriesUpper, tseriesLower) = Abber.Batch(series, 10);
|
||||
|
||||
// Calculate with Span API
|
||||
double[] spanMiddle = new double[100];
|
||||
double[] spanUpper = new double[100];
|
||||
double[] spanLower = new double[100];
|
||||
Abber.Batch(source.AsSpan(), spanMiddle.AsSpan(), spanUpper.AsSpan(), spanLower.AsSpan(), 10);
|
||||
|
||||
// Compare results
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
Assert.Equal(tseriesMiddle[i].Value, spanMiddle[i], 1e-10);
|
||||
Assert.Equal(tseriesUpper[i].Value, spanUpper[i], 1e-10);
|
||||
Assert.Equal(tseriesLower[i].Value, spanLower[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_SpanBatch_ZeroAllocation()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
double[] source = new double[10000];
|
||||
double[] middle = new double[10000];
|
||||
double[] upper = new double[10000];
|
||||
double[] lower = new double[10000];
|
||||
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
source[i] = gbm.Next().Close;
|
||||
}
|
||||
|
||||
// Warm up
|
||||
Abber.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 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 Abber_SpanBatch_HandlesNaN()
|
||||
{
|
||||
double[] source = [100, 110, double.NaN, 130, 140];
|
||||
double[] middle = new double[5];
|
||||
double[] upper = new double[5];
|
||||
double[] lower = new double[5];
|
||||
|
||||
Abber.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 3);
|
||||
|
||||
// All outputs should be finite
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(middle[i]), $"Middle[{i}] expected finite but got {middle[i]}");
|
||||
Assert.True(double.IsFinite(upper[i]), $"Upper[{i}] expected finite but got {upper[i]}");
|
||||
Assert.True(double.IsFinite(lower[i]), $"Lower[{i}] expected finite but got {lower[i]}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_AllModes_ProduceSameResult()
|
||||
{
|
||||
// Arrange
|
||||
int period = 10;
|
||||
double multiplier = 2.0;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
// 1. Batch Mode
|
||||
var (batchMiddle, batchUpper, batchLower) = Abber.Batch(series, period, multiplier);
|
||||
double expectedMiddle = batchMiddle.Last.Value;
|
||||
double expectedUpper = batchUpper.Last.Value;
|
||||
double expectedLower = batchLower.Last.Value;
|
||||
|
||||
// 2. Span Mode
|
||||
double[] source = series.Values.ToArray();
|
||||
double[] spanMiddle = new double[series.Count];
|
||||
double[] spanUpper = new double[series.Count];
|
||||
double[] spanLower = new double[series.Count];
|
||||
Abber.Batch(source.AsSpan(), spanMiddle.AsSpan(), spanUpper.AsSpan(), spanLower.AsSpan(), period, multiplier);
|
||||
|
||||
// 3. Streaming Mode
|
||||
var streamingInd = new Abber(period, multiplier);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
streamingInd.Update(series[i]);
|
||||
}
|
||||
double streamingMiddle = streamingInd.Last.Value;
|
||||
double streamingUpper = streamingInd.Upper.Value;
|
||||
double streamingLower = streamingInd.Lower.Value;
|
||||
|
||||
// 4. Eventing Mode
|
||||
var pubSource = new TSeries();
|
||||
var eventingInd = new Abber(pubSource, period, multiplier);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
pubSource.Add(series[i]);
|
||||
}
|
||||
double eventingMiddle = eventingInd.Last.Value;
|
||||
double eventingUpper = eventingInd.Upper.Value;
|
||||
double eventingLower = eventingInd.Lower.Value;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedMiddle, spanMiddle[^1], precision: 9);
|
||||
Assert.Equal(expectedUpper, spanUpper[^1], precision: 9);
|
||||
Assert.Equal(expectedLower, spanLower[^1], precision: 9);
|
||||
|
||||
Assert.Equal(expectedMiddle, streamingMiddle, precision: 9);
|
||||
Assert.Equal(expectedUpper, streamingUpper, precision: 9);
|
||||
Assert.Equal(expectedLower, streamingLower, precision: 9);
|
||||
|
||||
Assert.Equal(expectedMiddle, eventingMiddle, precision: 9);
|
||||
Assert.Equal(expectedUpper, eventingUpper, precision: 9);
|
||||
Assert.Equal(expectedLower, eventingLower, precision: 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_Chainability_Works()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var abber = new Abber(source, 10);
|
||||
|
||||
source.Add(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.Equal(100, abber.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_WarmupPeriod_IsSetCorrectly()
|
||||
{
|
||||
var abber = new Abber(10);
|
||||
Assert.Equal(10, abber.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_Prime_SetsStateCorrectly()
|
||||
{
|
||||
var abber = new Abber(3, 2.0);
|
||||
var series = new TSeries();
|
||||
|
||||
// Add 5 values
|
||||
series.Add(DateTime.UtcNow, 100);
|
||||
series.Add(DateTime.UtcNow, 110);
|
||||
series.Add(DateTime.UtcNow, 120);
|
||||
series.Add(DateTime.UtcNow, 130);
|
||||
series.Add(DateTime.UtcNow, 140);
|
||||
|
||||
abber.Prime(series);
|
||||
|
||||
Assert.True(abber.IsHot);
|
||||
|
||||
// Last 3 values: 120, 130, 140 -> SMA = 130
|
||||
Assert.Equal(130.0, abber.Last.Value, 1e-10);
|
||||
|
||||
// Verify it continues correctly
|
||||
abber.Update(new TValue(DateTime.UtcNow, 150));
|
||||
// New window: 130, 140, 150 -> SMA = 140
|
||||
Assert.Equal(140.0, abber.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_Calculate_ReturnsCorrectResultsAndHotIndicator()
|
||||
{
|
||||
var series = new TSeries();
|
||||
series.Add(DateTime.UtcNow, 100);
|
||||
series.Add(DateTime.UtcNow, 110);
|
||||
series.Add(DateTime.UtcNow, 120);
|
||||
series.Add(DateTime.UtcNow, 130);
|
||||
series.Add(DateTime.UtcNow, 140);
|
||||
|
||||
var ((middle, upper, lower), indicator) = Abber.Calculate(series, 3, 2.0);
|
||||
|
||||
// Check results
|
||||
Assert.Equal(5, middle.Count);
|
||||
Assert.Equal(5, upper.Count);
|
||||
Assert.Equal(5, lower.Count);
|
||||
|
||||
// Check indicator state
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.Equal(130.0, indicator.Last.Value, 1e-10);
|
||||
Assert.Equal(3, indicator.WarmupPeriod);
|
||||
|
||||
// Verify indicator continues correctly
|
||||
indicator.Update(new TValue(DateTime.UtcNow, 150));
|
||||
Assert.Equal(140.0, indicator.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_DifferentMultipliers_Work()
|
||||
{
|
||||
var series = new TSeries();
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
series.Add(DateTime.UtcNow, 100 + i * 10); // 100, 110, 120, ...
|
||||
}
|
||||
|
||||
// Multiplier 1.0
|
||||
var (middle1, upper1, _) = Abber.Batch(series, 5, 1.0);
|
||||
|
||||
// Multiplier 3.0
|
||||
var (middle3, upper3, _) = Abber.Batch(series, 5, 3.0);
|
||||
|
||||
// Middle should be the same for all multipliers
|
||||
Assert.Equal(middle1.Last.Value, middle3.Last.Value, 1e-10);
|
||||
|
||||
// Band width should scale with multiplier
|
||||
double bandWidth1 = upper1.Last.Value - middle1.Last.Value;
|
||||
double bandWidth3 = upper3.Last.Value - middle3.Last.Value;
|
||||
Assert.Equal(bandWidth1 * 3.0, bandWidth3, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_FlatLine_ReturnsSameValues()
|
||||
{
|
||||
var abber = new Abber(10);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
abber.Update(new TValue(DateTime.UtcNow, 100));
|
||||
}
|
||||
|
||||
// When all values are the same, SMA = 100, all deviations = 0
|
||||
Assert.Equal(100.0, abber.Last.Value, 1e-10);
|
||||
Assert.Equal(100.0, abber.Upper.Value, 1e-10);
|
||||
Assert.Equal(100.0, abber.Lower.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_Pub_EventFires()
|
||||
{
|
||||
var abber = new Abber(10);
|
||||
bool eventFired = false;
|
||||
abber.Pub += (object? sender, in TValueEventArgs args) => eventFired = true;
|
||||
|
||||
abber.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.True(eventFired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abber_BandsAreSymmetric()
|
||||
{
|
||||
var abber = new Abber(10, 2.0);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
abber.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
// Upper - Middle should equal Middle - Lower
|
||||
double upperDiff = abber.Upper.Value - abber.Last.Value;
|
||||
double lowerDiff = abber.Last.Value - abber.Lower.Value;
|
||||
|
||||
Assert.Equal(upperDiff, lowerDiff, 1e-10);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for Abber indicator.
|
||||
/// Note: Skender.Stock.Indicators, TA-Lib, Tulip, and OoplesFinance do not provide
|
||||
/// Abber (Aberration Bands) implementation for cross-validation. These tests validate
|
||||
/// against manual calculations and internal consistency across all API modes.
|
||||
/// </summary>
|
||||
public sealed class AbberValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _testData;
|
||||
private readonly ITestOutputHelper _output;
|
||||
private bool _disposed;
|
||||
|
||||
public AbberValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
_testData = new ValidationTestData();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_testData?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ManualCalculation_Period3()
|
||||
{
|
||||
// Manual calculation verification
|
||||
// Values: [100, 110, 120]
|
||||
// Bar 1: SMA=100, Dev=0, AvgDev=0
|
||||
// Bar 2: SMA=(100+110)/2=105, Dev1=0, Dev2=|110-100|=10, AvgDev=(0+10)/2=5
|
||||
// Bar 3: SMA=(100+110+120)/3=110, Dev3=|120-105|=15, AvgDev=(0+10+15)/3=8.333
|
||||
|
||||
var series = new TSeries();
|
||||
var time = DateTime.UtcNow;
|
||||
series.Add(new TValue(time, 100));
|
||||
series.Add(new TValue(time.AddMinutes(1), 110));
|
||||
series.Add(new TValue(time.AddMinutes(2), 120));
|
||||
|
||||
var abber = new Abber(3, 2.0);
|
||||
var (middle, upper, lower) = abber.Update(series);
|
||||
|
||||
// SMA(3) = 110
|
||||
Assert.Equal(110.0, middle.Last.Value, 1e-10);
|
||||
|
||||
// AvgDev = (0 + 10 + 15) / 3 = 25/3
|
||||
double expectedAvgDev = 25.0 / 3.0;
|
||||
double expectedBandWidth = 2.0 * expectedAvgDev;
|
||||
|
||||
Assert.Equal(110.0 + expectedBandWidth, upper.Last.Value, 1e-10);
|
||||
Assert.Equal(110.0 - expectedBandWidth, lower.Last.Value, 1e-10);
|
||||
|
||||
_output.WriteLine("Abber manual calculation (period 3) validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ManualCalculation_Period5()
|
||||
{
|
||||
// Manual calculation verification with period 5
|
||||
// Use simple arithmetic sequence: 100, 110, 120, 130, 140
|
||||
var series = new TSeries();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
double[] values = { 100, 110, 120, 130, 140 };
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
series.Add(new TValue(time.AddMinutes(i), values[i]));
|
||||
}
|
||||
|
||||
var abber = new Abber(5, 2.0);
|
||||
var (middle, _, _) = abber.Update(series);
|
||||
|
||||
// SMA(5) = (100 + 110 + 120 + 130 + 140) / 5 = 120
|
||||
Assert.Equal(120.0, middle.Last.Value, 1e-10);
|
||||
|
||||
_output.WriteLine("Abber manual calculation (period 5) validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Multiplier_Effect()
|
||||
{
|
||||
// Verify multiplier affects band width correctly
|
||||
var series = new TSeries();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
// Oscillating values to create deviation
|
||||
double value = 100 + (i % 2 == 0 ? 10 : -10);
|
||||
series.Add(new TValue(time.AddMinutes(i), value));
|
||||
}
|
||||
|
||||
var (middle1, upper1, _) = Abber.Batch(series, 10, 1.0);
|
||||
var (middle2, upper2, _) = Abber.Batch(series, 10, 2.0);
|
||||
var (middle3, upper3, _) = Abber.Batch(series, 10, 3.0);
|
||||
|
||||
// Middle should be the same regardless of multiplier
|
||||
Assert.Equal(middle1.Last.Value, middle2.Last.Value, 1e-10);
|
||||
Assert.Equal(middle2.Last.Value, middle3.Last.Value, 1e-10);
|
||||
|
||||
// Band widths should scale linearly with multiplier
|
||||
double bw1 = upper1.Last.Value - middle1.Last.Value;
|
||||
double bw2 = upper2.Last.Value - middle2.Last.Value;
|
||||
double bw3 = upper3.Last.Value - middle3.Last.Value;
|
||||
|
||||
Assert.Equal(bw1 * 2.0, bw2, 1e-10);
|
||||
Assert.Equal(bw1 * 3.0, bw3, 1e-10);
|
||||
|
||||
_output.WriteLine("Abber multiplier effect validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_AllModes_Consistency_Batch()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Batch mode using instance
|
||||
var abber = new Abber(period, 2.0);
|
||||
var (qMiddle, qUpper, qLower) = abber.Update(_testData.Data);
|
||||
|
||||
// Static batch
|
||||
var (sMiddle, sUpper, sLower) = Abber.Batch(_testData.Data, period, 2.0);
|
||||
|
||||
// Verify match
|
||||
ValidationHelper.VerifySeriesEqual(qMiddle, sMiddle);
|
||||
ValidationHelper.VerifySeriesEqual(qUpper, sUpper);
|
||||
ValidationHelper.VerifySeriesEqual(qLower, sLower);
|
||||
}
|
||||
_output.WriteLine("Abber Batch modes consistency validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_AllModes_Consistency_Streaming()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Streaming mode
|
||||
var streamingAbber = new Abber(period, 2.0);
|
||||
var streamMiddle = new TSeries();
|
||||
var streamUpper = new TSeries();
|
||||
var streamLower = new TSeries();
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
streamingAbber.Update(item);
|
||||
streamMiddle.Add(streamingAbber.Last);
|
||||
streamUpper.Add(streamingAbber.Upper);
|
||||
streamLower.Add(streamingAbber.Lower);
|
||||
}
|
||||
|
||||
// Batch mode for comparison
|
||||
var (batchMiddle, batchUpper, batchLower) = Abber.Batch(_testData.Data, period, 2.0);
|
||||
|
||||
// Verify match
|
||||
ValidationHelper.VerifySeriesEqual(batchMiddle, streamMiddle);
|
||||
ValidationHelper.VerifySeriesEqual(batchUpper, streamUpper);
|
||||
ValidationHelper.VerifySeriesEqual(batchLower, streamLower);
|
||||
}
|
||||
_output.WriteLine("Abber Streaming mode consistency validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_AllModes_Consistency_Span()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
double[] source = _testData.RawData.ToArray();
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Span mode
|
||||
int len = source.Length;
|
||||
double[] spanMiddle = new double[len];
|
||||
double[] spanUpper = new double[len];
|
||||
double[] spanLower = new double[len];
|
||||
|
||||
Abber.Batch(source.AsSpan(), spanMiddle.AsSpan(), spanUpper.AsSpan(), spanLower.AsSpan(),
|
||||
period, 2.0);
|
||||
|
||||
// Batch mode for comparison
|
||||
var (batchMiddle, batchUpper, batchLower) = Abber.Batch(_testData.Data, period, 2.0);
|
||||
|
||||
// Verify match
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Assert.Equal(batchMiddle[i].Value, spanMiddle[i], 9);
|
||||
Assert.Equal(batchUpper[i].Value, spanUpper[i], 9);
|
||||
Assert.Equal(batchLower[i].Value, spanLower[i], 9);
|
||||
}
|
||||
}
|
||||
_output.WriteLine("Abber Span mode consistency validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_AllModes_Consistency_Eventing()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Eventing mode
|
||||
var pubSource = new TSeries();
|
||||
var eventingInd = new Abber(pubSource, period, 2.0);
|
||||
var eventMiddle = new TSeries();
|
||||
var eventUpper = new TSeries();
|
||||
var eventLower = new TSeries();
|
||||
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
pubSource.Add(item);
|
||||
eventMiddle.Add(eventingInd.Last);
|
||||
eventUpper.Add(eventingInd.Upper);
|
||||
eventLower.Add(eventingInd.Lower);
|
||||
}
|
||||
|
||||
// Batch mode for comparison
|
||||
var (batchMiddle, batchUpper, batchLower) = Abber.Batch(_testData.Data, period, 2.0);
|
||||
|
||||
// Verify match
|
||||
ValidationHelper.VerifySeriesEqual(batchMiddle, eventMiddle);
|
||||
ValidationHelper.VerifySeriesEqual(batchUpper, eventUpper);
|
||||
ValidationHelper.VerifySeriesEqual(batchLower, eventLower);
|
||||
}
|
||||
_output.WriteLine("Abber Eventing mode consistency validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Calculate_ReturnsHotIndicator()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var ((_, _, _), indicator) = Abber.Calculate(_testData.Data, period, 2.0);
|
||||
|
||||
// Verify indicator is hot
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.Equal(period, indicator.WarmupPeriod);
|
||||
|
||||
// Note: Indicator state after Prime may not exactly match batch output because
|
||||
// deviation calculations depend on SMA history. Prime only restores the last
|
||||
// WarmupPeriod bars, so deviations are calculated differently.
|
||||
// We verify the indicator is in a valid state for continued streaming.
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
Assert.True(double.IsFinite(indicator.Upper.Value));
|
||||
Assert.True(double.IsFinite(indicator.Lower.Value));
|
||||
|
||||
// Verify can continue streaming
|
||||
var nextValue = new TValue(DateTime.UtcNow.AddDays(1), 100);
|
||||
indicator.Update(nextValue);
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
_output.WriteLine("Abber Calculate method validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_LargeDataset_NoOverflow()
|
||||
{
|
||||
// Test with the full 5000 bar dataset
|
||||
var (middle, upper, lower) = Abber.Batch(_testData.Data, 100, 2.0);
|
||||
|
||||
// All outputs should be finite
|
||||
ValidationHelper.VerifyAllFinite(middle, startIndex: 0);
|
||||
ValidationHelper.VerifyAllFinite(upper, startIndex: 0);
|
||||
ValidationHelper.VerifyAllFinite(lower, startIndex: 0);
|
||||
|
||||
// Upper should always be >= Middle, Middle should always be >= Lower
|
||||
for (int i = 100; i < middle.Count; i++)
|
||||
{
|
||||
Assert.True(upper[i].Value >= middle[i].Value,
|
||||
$"Upper ({upper[i].Value}) should be >= Middle ({middle[i].Value}) at index {i}");
|
||||
Assert.True(middle[i].Value >= lower[i].Value,
|
||||
$"Middle ({middle[i].Value}) should be >= Lower ({lower[i].Value}) at index {i}");
|
||||
}
|
||||
|
||||
_output.WriteLine("Abber large dataset (5000 bars) validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_BandWidth_IsSymmetric()
|
||||
{
|
||||
// Verify that Upper - Middle == Middle - Lower
|
||||
// This confirms the band width is applied symmetrically
|
||||
|
||||
var (middle, upper, lower) = Abber.Batch(_testData.Data, 20, 2.0);
|
||||
|
||||
// After warmup, verify symmetry
|
||||
for (int i = 20; i < _testData.Data.Count; i++)
|
||||
{
|
||||
double upperDiff = upper[i].Value - middle[i].Value;
|
||||
double lowerDiff = middle[i].Value - lower[i].Value;
|
||||
|
||||
Assert.Equal(upperDiff, lowerDiff, 1e-9);
|
||||
}
|
||||
|
||||
_output.WriteLine("Abber band width symmetry validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Prime_ProducesCorrectState()
|
||||
{
|
||||
// Prime with history and verify state matches full calculation
|
||||
int period = 20;
|
||||
|
||||
// Full batch calculation
|
||||
var (batchMiddle, batchUpper, batchLower) = Abber.Batch(_testData.Data, period, 2.0);
|
||||
|
||||
// Prime indicator with subset and continue
|
||||
var primedIndicator = new Abber(period, 2.0);
|
||||
var subset = new TSeries();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
subset.Add(_testData.Data[i]);
|
||||
}
|
||||
primedIndicator.Prime(subset);
|
||||
|
||||
// Continue streaming from where Prime left off
|
||||
for (int i = 100; i < _testData.Data.Count; i++)
|
||||
{
|
||||
primedIndicator.Update(_testData.Data[i]);
|
||||
}
|
||||
|
||||
// Final values should match
|
||||
Assert.Equal(batchMiddle.Last.Value, primedIndicator.Last.Value, 1e-9);
|
||||
Assert.Equal(batchUpper.Last.Value, primedIndicator.Upper.Value, 1e-9);
|
||||
Assert.Equal(batchLower.Last.Value, primedIndicator.Lower.Value, 1e-9);
|
||||
|
||||
_output.WriteLine("Abber Prime method validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_MiddleBand_MatchesSMA()
|
||||
{
|
||||
// Verify the middle band is exactly the SMA
|
||||
int period = 20;
|
||||
|
||||
var abber = new Abber(period, 2.0);
|
||||
var sma = new Sma(period);
|
||||
|
||||
var abberResults = abber.Update(_testData.Data);
|
||||
var smaResults = sma.Update(_testData.Data);
|
||||
|
||||
// Middle band should match SMA exactly
|
||||
for (int i = 0; i < _testData.Data.Count; i++)
|
||||
{
|
||||
Assert.Equal(smaResults[i].Value, abberResults.Middle[i].Value, 1e-10);
|
||||
}
|
||||
|
||||
_output.WriteLine("Abber middle band matches SMA validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_DeviationCalculation()
|
||||
{
|
||||
// Verify the deviation is calculated as |source - SMA|
|
||||
int period = 5;
|
||||
|
||||
// Use predictable values
|
||||
var series = new TSeries();
|
||||
var time = DateTime.UtcNow;
|
||||
double[] values = { 100, 120, 80, 110, 90 };
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
series.Add(new TValue(time.AddMinutes(i), values[i]));
|
||||
}
|
||||
|
||||
var abber = new Abber(period, 1.0); // multiplier = 1 for easier verification
|
||||
var (middle, upper, _) = abber.Update(series);
|
||||
|
||||
// SMA(5) = (100 + 120 + 80 + 110 + 90) / 5 = 100
|
||||
Assert.Equal(100.0, middle.Last.Value, 1e-10);
|
||||
|
||||
// Band width = AvgDeviation (since multiplier = 1)
|
||||
// The deviations are calculated incrementally, so we verify the final result
|
||||
double bandWidth = upper.Last.Value - middle.Last.Value;
|
||||
Assert.True(bandWidth >= 0, "Band width should be non-negative");
|
||||
Assert.True(double.IsFinite(bandWidth), "Band width should be finite");
|
||||
|
||||
_output.WriteLine("Abber deviation calculation validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Consistency_AcrossPeriods()
|
||||
{
|
||||
// Verify behavior is consistent across different periods
|
||||
int[] periods = { 3, 5, 10, 20, 50, 100, 200 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var (middle, upper, lower) = Abber.Batch(_testData.Data, period, 2.0);
|
||||
|
||||
// All values should be finite
|
||||
for (int i = 0; i < middle.Count; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(middle[i].Value), $"Middle[{i}] not finite for period {period}");
|
||||
Assert.True(double.IsFinite(upper[i].Value), $"Upper[{i}] not finite for period {period}");
|
||||
Assert.True(double.IsFinite(lower[i].Value), $"Lower[{i}] not finite for period {period}");
|
||||
}
|
||||
|
||||
// Upper >= Middle >= Lower (bands are symmetric around middle)
|
||||
for (int i = period; i < middle.Count; i++)
|
||||
{
|
||||
Assert.True(upper[i].Value >= middle[i].Value);
|
||||
Assert.True(middle[i].Value >= lower[i].Value);
|
||||
}
|
||||
}
|
||||
|
||||
_output.WriteLine($"Abber consistency across {periods.Length} periods validated successfully");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,682 @@
|
||||
using System.Buffers;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Abber: Aberration Bands
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Aberration Bands measure price deviation from a central moving average using absolute
|
||||
/// deviation rather than standard deviation. This approach provides more intuitive and
|
||||
/// outlier-resistant bands compared to Bollinger Bands.
|
||||
///
|
||||
/// Calculation:
|
||||
/// Middle Band = SMA(Source, Period)
|
||||
/// Deviation = |Source - Middle|
|
||||
/// Average Deviation = SMA(Deviation, Period)
|
||||
/// Upper Band = Middle + (Multiplier x Average Deviation)
|
||||
/// Lower Band = Middle - (Multiplier x Average Deviation)
|
||||
///
|
||||
/// Key characteristics:
|
||||
/// - Uses absolute deviation instead of standard deviation
|
||||
/// - Less sensitive to extreme outliers than Bollinger Bands
|
||||
/// - Provides intuitive measure of typical price dispersion
|
||||
/// - Bands expand during volatile periods and contract during consolidation
|
||||
///
|
||||
/// Sources:
|
||||
/// Pine Script implementation: https://github.com/mihakralj/pinescript/blob/main/indicators/channels/abber.pine
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Abber : ITValuePublisher
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly double _multiplier;
|
||||
private readonly RingBuffer _sourceBuffer;
|
||||
private readonly RingBuffer _deviationBuffer;
|
||||
private readonly TValuePublishedHandler _handler;
|
||||
|
||||
private const int ResyncInterval = 1000;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
double SumSource,
|
||||
double SumDeviation,
|
||||
double LastValidValue,
|
||||
int TickCount
|
||||
);
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
|
||||
/// <summary>
|
||||
/// Display name for the indicator.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of periods before the indicator is considered "hot" (valid).
|
||||
/// </summary>
|
||||
public int WarmupPeriod { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Current middle band value (SMA of source).
|
||||
/// </summary>
|
||||
public TValue Last { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current upper band value.
|
||||
/// </summary>
|
||||
public TValue Upper { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current lower band value.
|
||||
/// </summary>
|
||||
public TValue Lower { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the indicator has enough data to produce valid results.
|
||||
/// </summary>
|
||||
public bool IsHot => _sourceBuffer.IsFull;
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when a new TValue is available.
|
||||
/// </summary>
|
||||
public event TValuePublishedHandler? Pub;
|
||||
|
||||
/// <summary>
|
||||
/// Creates Abber with specified period and multiplier.
|
||||
/// </summary>
|
||||
/// <param name="period">Lookback period for SMA and deviation calculations (must be > 0)</param>
|
||||
/// <param name="multiplier">Multiplier for band width (must be > 0, default: 2.0)</param>
|
||||
public Abber(int period, double multiplier = 2.0)
|
||||
{
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
if (multiplier <= 0)
|
||||
throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier));
|
||||
|
||||
_period = period;
|
||||
_multiplier = multiplier;
|
||||
_sourceBuffer = new RingBuffer(period);
|
||||
_deviationBuffer = new RingBuffer(period);
|
||||
Name = $"Abber({period},{multiplier:F2})";
|
||||
WarmupPeriod = period;
|
||||
_handler = HandleValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates Abber with TSeries source.
|
||||
/// </summary>
|
||||
public Abber(TSeries source, int period, double multiplier = 2.0) : this(period, multiplier)
|
||||
{
|
||||
Prime(source);
|
||||
source.Pub += _handler;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates Abber with ITValuePublisher source.
|
||||
/// </summary>
|
||||
public Abber(ITValuePublisher source, int period, double multiplier = 2.0) : this(period, multiplier)
|
||||
{
|
||||
source.Pub += _handler;
|
||||
}
|
||||
|
||||
private void HandleValue(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
/// <summary>
|
||||
/// Helper to invoke the Pub event.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void PubEvent(TValue value, bool isNew = true)
|
||||
{
|
||||
Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a valid input value, using last-value substitution for non-finite inputs.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double GetValidValue(double input)
|
||||
{
|
||||
if (double.IsFinite(input))
|
||||
{
|
||||
_state.LastValidValue = input;
|
||||
return input;
|
||||
}
|
||||
return _state.LastValidValue;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void UpdateState(double value, double deviation)
|
||||
{
|
||||
double removedSource = _sourceBuffer.Count == _sourceBuffer.Capacity ? _sourceBuffer.Oldest : 0.0;
|
||||
double removedDeviation = _deviationBuffer.Count == _deviationBuffer.Capacity ? _deviationBuffer.Oldest : 0.0;
|
||||
|
||||
_state.SumSource = _state.SumSource - removedSource + value;
|
||||
_state.SumDeviation = _state.SumDeviation - removedDeviation + deviation;
|
||||
|
||||
_sourceBuffer.Add(value);
|
||||
_deviationBuffer.Add(deviation);
|
||||
|
||||
_state.TickCount++;
|
||||
if (_sourceBuffer.IsFull && _state.TickCount >= ResyncInterval)
|
||||
{
|
||||
_state.TickCount = 0;
|
||||
_state.SumSource = _sourceBuffer.RecalculateSum();
|
||||
_state.SumDeviation = _deviationBuffer.RecalculateSum();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a TValue input.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
double value = GetValidValue(input.Value);
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
_p_state = _state;
|
||||
|
||||
// Calculate SMA first to get deviation
|
||||
int count = _sourceBuffer.Count;
|
||||
double sma = count > 0 ? _state.SumSource / count : value;
|
||||
double deviation = Math.Abs(value - sma);
|
||||
|
||||
UpdateState(value, deviation);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = _p_state;
|
||||
|
||||
// Calculate SMA first to get deviation
|
||||
int count = _sourceBuffer.Count;
|
||||
double sma = count > 0 ? _state.SumSource / count : value;
|
||||
double deviation = Math.Abs(value - sma);
|
||||
|
||||
_sourceBuffer.UpdateNewest(value);
|
||||
_deviationBuffer.UpdateNewest(deviation);
|
||||
|
||||
_state = _state with
|
||||
{
|
||||
SumSource = _sourceBuffer.Sum,
|
||||
SumDeviation = _deviationBuffer.Sum
|
||||
};
|
||||
}
|
||||
|
||||
int currentCount = _sourceBuffer.Count;
|
||||
if (currentCount == 0)
|
||||
{
|
||||
Last = new TValue(input.Time, double.NaN);
|
||||
Upper = new TValue(input.Time, double.NaN);
|
||||
Lower = new TValue(input.Time, double.NaN);
|
||||
}
|
||||
else
|
||||
{
|
||||
double middle = _state.SumSource / currentCount;
|
||||
double avgDeviation = _state.SumDeviation / currentCount;
|
||||
double bandWidth = _multiplier * avgDeviation;
|
||||
|
||||
Last = new TValue(input.Time, middle);
|
||||
Upper = new TValue(input.Time, middle + bandWidth);
|
||||
Lower = new TValue(input.Time, middle - bandWidth);
|
||||
}
|
||||
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a TSeries.
|
||||
/// </summary>
|
||||
public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
return (new TSeries([], []), new TSeries([], []), new TSeries([], []));
|
||||
|
||||
int len = source.Count;
|
||||
var tMiddle = new List<long>(len);
|
||||
var vMiddle = new List<double>(len);
|
||||
var tUpper = new List<long>(len);
|
||||
var vUpper = new List<double>(len);
|
||||
var tLower = new List<long>(len);
|
||||
var vLower = new List<double>(len);
|
||||
|
||||
CollectionsMarshal.SetCount(tMiddle, len);
|
||||
CollectionsMarshal.SetCount(vMiddle, len);
|
||||
CollectionsMarshal.SetCount(tUpper, len);
|
||||
CollectionsMarshal.SetCount(vUpper, len);
|
||||
CollectionsMarshal.SetCount(tLower, len);
|
||||
CollectionsMarshal.SetCount(vLower, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(tMiddle);
|
||||
var vMiddleSpan = CollectionsMarshal.AsSpan(vMiddle);
|
||||
var vUpperSpan = CollectionsMarshal.AsSpan(vUpper);
|
||||
var vLowerSpan = CollectionsMarshal.AsSpan(vLower);
|
||||
|
||||
// Use batch calculation
|
||||
Batch(source.Values, vMiddleSpan, vUpperSpan, vLowerSpan, _period, _multiplier);
|
||||
|
||||
source.Times.CopyTo(tSpan);
|
||||
|
||||
// Copy timestamps to upper and lower (same time series)
|
||||
tSpan.CopyTo(CollectionsMarshal.AsSpan(tUpper));
|
||||
tSpan.CopyTo(CollectionsMarshal.AsSpan(tLower));
|
||||
|
||||
// Prime the state for continued streaming
|
||||
Prime(source);
|
||||
|
||||
return (new TSeries(tMiddle, vMiddle), new TSeries(tUpper, vUpper), new TSeries(tLower, vLower));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the indicator state using the provided TSeries history.
|
||||
/// </summary>
|
||||
public void Prime(TSeries source)
|
||||
{
|
||||
if (source.Count == 0) return;
|
||||
|
||||
// Reset state
|
||||
_sourceBuffer.Clear();
|
||||
_deviationBuffer.Clear();
|
||||
_state = default;
|
||||
_p_state = default;
|
||||
|
||||
int warmupLength = Math.Min(source.Count, WarmupPeriod);
|
||||
int startIndex = source.Count - warmupLength;
|
||||
|
||||
// Seed LastValidValue
|
||||
_state.LastValidValue = double.NaN;
|
||||
|
||||
for (int i = startIndex - 1; i >= 0; i--)
|
||||
{
|
||||
if (double.IsFinite(source[i].Value))
|
||||
{
|
||||
_state.LastValidValue = source[i].Value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Find valid value in warmup window if not found
|
||||
if (double.IsNaN(_state.LastValidValue))
|
||||
{
|
||||
for (int i = startIndex; i < source.Count; i++)
|
||||
{
|
||||
if (double.IsFinite(source[i].Value))
|
||||
{
|
||||
_state.LastValidValue = source[i].Value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Feed the buffers
|
||||
for (int i = startIndex; i < source.Count; i++)
|
||||
{
|
||||
double value = GetValidValue(source[i].Value);
|
||||
|
||||
// Calculate SMA to get deviation
|
||||
int count = _sourceBuffer.Count;
|
||||
double sma = count > 0 ? _state.SumSource / count : value;
|
||||
double deviation = Math.Abs(value - sma);
|
||||
|
||||
UpdateState(value, deviation);
|
||||
}
|
||||
|
||||
// Finalize state
|
||||
int currentCount = _sourceBuffer.Count;
|
||||
if (currentCount > 0)
|
||||
{
|
||||
var lastItem = source.Last;
|
||||
double middle = _state.SumSource / currentCount;
|
||||
double avgDeviation = _state.SumDeviation / currentCount;
|
||||
double bandWidth = _multiplier * avgDeviation;
|
||||
|
||||
Last = new TValue(lastItem.Time, middle);
|
||||
Upper = new TValue(lastItem.Time, middle + bandWidth);
|
||||
Lower = new TValue(lastItem.Time, middle - bandWidth);
|
||||
}
|
||||
|
||||
_p_state = _state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the indicator state.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
_sourceBuffer.Clear();
|
||||
_deviationBuffer.Clear();
|
||||
_state = default;
|
||||
_p_state = default;
|
||||
Last = default;
|
||||
Upper = default;
|
||||
Lower = default;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Static Batch Methods
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/// <summary>
|
||||
/// Output buffers for batch Abber calculation.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
#pragma warning disable S1104 // Fields should not have public accessibility
|
||||
public ref struct BatchOutputs
|
||||
{
|
||||
/// <summary>Output middle band (SMA of source)</summary>
|
||||
public Span<double> Middle;
|
||||
/// <summary>Output upper band</summary>
|
||||
public Span<double> Upper;
|
||||
/// <summary>Output lower band</summary>
|
||||
public Span<double> Lower;
|
||||
#pragma warning restore S1104
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new BatchOutputs instance.
|
||||
/// </summary>
|
||||
public BatchOutputs(Span<double> middle, Span<double> upper, Span<double> lower)
|
||||
{
|
||||
Middle = middle;
|
||||
Upper = upper;
|
||||
Lower = lower;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal state for scalar calculation.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private ref struct ScalarState
|
||||
{
|
||||
public double SumSource;
|
||||
public double SumDeviation;
|
||||
public double LastValidValue;
|
||||
public int SourceBufferIndex;
|
||||
public int DeviationBufferIndex;
|
||||
public int TickCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Working buffers for batch calculation.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private ref struct WorkBuffers
|
||||
{
|
||||
public Span<double> Source;
|
||||
public Span<double> Deviation;
|
||||
|
||||
public WorkBuffers(Span<double> source, Span<double> deviation)
|
||||
{
|
||||
Source = source;
|
||||
Deviation = deviation;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Abber for the entire TSeries using a new instance.
|
||||
/// </summary>
|
||||
public static (TSeries Middle, TSeries Upper, TSeries Lower) Batch(TSeries source, int period, double multiplier = 2.0)
|
||||
{
|
||||
var abber = new Abber(period, multiplier);
|
||||
return abber.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Abber in-place using spans for maximum performance.
|
||||
/// Zero-allocation method.
|
||||
/// </summary>
|
||||
/// <param name="source">Source price values</param>
|
||||
/// <param name="outputs">Output buffers for middle, upper, and lower bands</param>
|
||||
/// <param name="period">Lookback period</param>
|
||||
/// <param name="multiplier">Band width multiplier</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Batch(
|
||||
ReadOnlySpan<double> source,
|
||||
BatchOutputs outputs,
|
||||
int period,
|
||||
double multiplier = 2.0)
|
||||
{
|
||||
Batch(source, outputs.Middle, outputs.Upper, outputs.Lower, period, multiplier);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Abber in-place using spans for maximum performance.
|
||||
/// Zero-allocation method.
|
||||
/// </summary>
|
||||
/// <param name="source">Source price values</param>
|
||||
/// <param name="middle">Output middle band (SMA of source)</param>
|
||||
/// <param name="upper">Output upper band</param>
|
||||
/// <param name="lower">Output lower band</param>
|
||||
/// <param name="period">Lookback period</param>
|
||||
/// <param name="multiplier">Band width multiplier</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Batch(
|
||||
ReadOnlySpan<double> source,
|
||||
Span<double> middle,
|
||||
Span<double> upper,
|
||||
Span<double> lower,
|
||||
int period,
|
||||
double multiplier = 2.0)
|
||||
{
|
||||
int len = source.Length;
|
||||
if (middle.Length < len || upper.Length < len || lower.Length < len)
|
||||
throw new ArgumentException("Output buffers must be at least as long as input", nameof(middle));
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
if (multiplier <= 0)
|
||||
throw new ArgumentException("Multiplier must be greater than 0", nameof(multiplier));
|
||||
|
||||
if (len == 0) return;
|
||||
|
||||
// Scalar implementation with NaN handling
|
||||
var outputs = new BatchOutputs(middle, upper, lower);
|
||||
CalculateScalarCore(source, outputs, period, multiplier);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void CalculateScalarCore(
|
||||
ReadOnlySpan<double> source,
|
||||
scoped BatchOutputs outputs,
|
||||
int period,
|
||||
double multiplier)
|
||||
{
|
||||
int len = source.Length;
|
||||
|
||||
// Always use ArrayPool to avoid span scope safety issues with stackalloc + ref structs
|
||||
double[] rentedSource = ArrayPool<double>.Shared.Rent(period);
|
||||
double[] rentedDeviation = ArrayPool<double>.Shared.Rent(period);
|
||||
|
||||
try
|
||||
{
|
||||
var buffers = new WorkBuffers(
|
||||
rentedSource.AsSpan(0, period),
|
||||
rentedDeviation.AsSpan(0, period));
|
||||
|
||||
var state = new ScalarState
|
||||
{
|
||||
LastValidValue = double.NaN
|
||||
};
|
||||
|
||||
SeedFirstValidValue(source, ref state);
|
||||
|
||||
int warmupEnd = Math.Min(period, len);
|
||||
ProcessWarmupPhase(source, outputs, warmupEnd, multiplier, ref buffers, ref state);
|
||||
ProcessMainLoop(source, outputs, warmupEnd, period, multiplier, ref buffers, ref state);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<double>.Shared.Return(rentedSource);
|
||||
ArrayPool<double>.Shared.Return(rentedDeviation);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void SeedFirstValidValue(ReadOnlySpan<double> source, ref ScalarState state)
|
||||
{
|
||||
int len = source.Length;
|
||||
for (int k = 0; k < len; k++)
|
||||
{
|
||||
if (double.IsFinite(source[k]))
|
||||
{
|
||||
state.LastValidValue = source[k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double GetValidValue(ReadOnlySpan<double> source, int i, ref ScalarState state)
|
||||
{
|
||||
double v = source[i];
|
||||
if (double.IsFinite(v))
|
||||
{
|
||||
state.LastValidValue = v;
|
||||
return v;
|
||||
}
|
||||
return state.LastValidValue;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteBandOutputs(scoped BatchOutputs outputs, int i, double middle, double avgDeviation, double multiplier)
|
||||
{
|
||||
double bandWidth = multiplier * avgDeviation;
|
||||
outputs.Middle[i] = middle;
|
||||
outputs.Upper[i] = middle + bandWidth;
|
||||
outputs.Lower[i] = middle - bandWidth;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ProcessWarmupPhase(
|
||||
ReadOnlySpan<double> source,
|
||||
scoped BatchOutputs outputs,
|
||||
int warmupEnd,
|
||||
double multiplier,
|
||||
ref WorkBuffers buffers,
|
||||
ref ScalarState state)
|
||||
{
|
||||
for (int i = 0; i < warmupEnd; i++)
|
||||
{
|
||||
double v = GetValidValue(source, i, ref state);
|
||||
|
||||
// Calculate current SMA to get deviation
|
||||
int count = i;
|
||||
double sma = count > 0 ? state.SumSource / count : v;
|
||||
double deviation = Math.Abs(v - sma);
|
||||
|
||||
state.SumSource += v;
|
||||
state.SumDeviation += deviation;
|
||||
|
||||
buffers.Source[i] = v;
|
||||
buffers.Deviation[i] = deviation;
|
||||
|
||||
int newCount = i + 1;
|
||||
double middle = state.SumSource / newCount;
|
||||
double avgDeviation = state.SumDeviation / newCount;
|
||||
WriteBandOutputs(outputs, i, middle, avgDeviation, multiplier);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ProcessMainLoop(
|
||||
ReadOnlySpan<double> source,
|
||||
scoped BatchOutputs outputs,
|
||||
int startIndex,
|
||||
int period,
|
||||
double multiplier,
|
||||
ref WorkBuffers buffers,
|
||||
ref ScalarState state)
|
||||
{
|
||||
int len = source.Length;
|
||||
for (int i = startIndex; i < len; i++)
|
||||
{
|
||||
double v = GetValidValue(source, i, ref state);
|
||||
|
||||
// Calculate current SMA to get deviation
|
||||
double sma = state.SumSource / period;
|
||||
double deviation = Math.Abs(v - sma);
|
||||
|
||||
// Update source running sum
|
||||
state.SumSource = state.SumSource - buffers.Source[state.SourceBufferIndex] + v;
|
||||
buffers.Source[state.SourceBufferIndex] = v;
|
||||
|
||||
// Update deviation running sum
|
||||
state.SumDeviation = state.SumDeviation - buffers.Deviation[state.DeviationBufferIndex] + deviation;
|
||||
buffers.Deviation[state.DeviationBufferIndex] = deviation;
|
||||
|
||||
state.SourceBufferIndex++;
|
||||
if (state.SourceBufferIndex >= period) state.SourceBufferIndex = 0;
|
||||
state.DeviationBufferIndex++;
|
||||
if (state.DeviationBufferIndex >= period) state.DeviationBufferIndex = 0;
|
||||
|
||||
double middle = state.SumSource / period;
|
||||
double avgDeviation = state.SumDeviation / period;
|
||||
WriteBandOutputs(outputs, i, middle, avgDeviation, multiplier);
|
||||
|
||||
state.TickCount++;
|
||||
if (state.TickCount >= ResyncInterval)
|
||||
{
|
||||
ResyncSums(period, ref buffers, ref state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ResyncSums(int period, ref WorkBuffers buffers, ref ScalarState state)
|
||||
{
|
||||
state.TickCount = 0;
|
||||
|
||||
if (Vector.IsHardwareAccelerated && period >= Vector<double>.Count)
|
||||
{
|
||||
state.SumSource = SumSimd(buffers.Source);
|
||||
state.SumDeviation = SumSimd(buffers.Deviation);
|
||||
}
|
||||
else
|
||||
{
|
||||
double recalcSumSource = 0, recalcSumDeviation = 0;
|
||||
for (int k = 0; k < period; k++)
|
||||
{
|
||||
recalcSumSource += buffers.Source[k];
|
||||
recalcSumDeviation += buffers.Deviation[k];
|
||||
}
|
||||
state.SumSource = recalcSumSource;
|
||||
state.SumDeviation = recalcSumDeviation;
|
||||
}
|
||||
}
|
||||
|
||||
private static double SumSimd(ReadOnlySpan<double> source)
|
||||
{
|
||||
var sumVector = Vector<double>.Zero;
|
||||
int i = 0;
|
||||
int size = Vector<double>.Count;
|
||||
int len = source.Length;
|
||||
|
||||
for (; i <= len - size; i += size)
|
||||
{
|
||||
sumVector += new Vector<double>(source.Slice(i, size));
|
||||
}
|
||||
|
||||
double sum = Vector.Sum(sumVector);
|
||||
|
||||
for (; i < len; i++)
|
||||
{
|
||||
sum += source[i];
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a high-performance batch calculation and returns a "Hot" Abber instance.
|
||||
/// </summary>
|
||||
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Abber Indicator) Calculate(TSeries source, int period, double multiplier = 2.0)
|
||||
{
|
||||
var abber = new Abber(period, multiplier);
|
||||
var results = abber.Update(source);
|
||||
return (results, abber);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
# ABBER: Aberration Bands
|
||||
|
||||
> "Standard deviation punishes outliers twice: once when they happen, once when they distort everything else."
|
||||
|
||||
ABBER measures price deviation from a central moving average using absolute deviation rather than standard deviation. The result: dynamic bands that adapt to volatility while remaining robust against extreme outliers. Where Bollinger Bands amplify outliers through squaring, ABBER uses raw absolute differences. Bands respond to typical price behavior, not the occasional spike that yanks everything sideways.
|
||||
|
||||
## Historical Context
|
||||
|
||||
Aberration Bands emerged as a response to the statistical assumptions baked into Bollinger Bands. Standard deviation assumes normally distributed returns. Markets laugh at that assumption daily. Fat tails, volatility clustering, flash crashes: the squared-deviation approach treats these events as if they carry information about typical behavior. They do not.
|
||||
|
||||
The absolute deviation approach predates Bollinger's work (mean absolute deviation appears in early 20th-century statistics), but applying it to band construction arrived later, once practitioners grew tired of watching their bands blow out on single-bar anomalies. No single inventor claims credit. The technique spread through trading floors where robustness mattered more than textbook elegance.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
ABBER computes three outputs through running sums maintained in O(1) streaming time:
|
||||
|
||||
* **Middle Band**: Simple Moving Average of source price
|
||||
* **Upper Band**: Middle + (Multiplier × Average Absolute Deviation)
|
||||
* **Lower Band**: Middle − (Multiplier × Average Absolute Deviation)
|
||||
|
||||
The average absolute deviation represents typical distance price travels from the moving average. No squaring, no square roots. Just raw, intuitive dispersion.
|
||||
|
||||
### The Outlier Problem
|
||||
|
||||
Standard deviation squares each deviation before averaging, then takes the square root. A single bar 4σ from the mean contributes 16× more weight than a 1σ bar. In ABBER, that same outlier contributes only 4× more. The mathematical consequence: ABBER bands recover faster from shocks. They measure the market's normal breathing, not its occasional screams.
|
||||
|
||||
The physics analogy: standard deviation is a spring that stores energy quadratically. Push twice as hard, store four times the energy. ABBER is a linear damper. Push twice as hard, resist twice as hard. Different behaviors, different use cases.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### 1. Middle Band
|
||||
|
||||
$$\text{Middle}_t = \frac{1}{n} \sum_{i=0}^{n-1} \text{Source}_{t-i}$$
|
||||
|
||||
### 2. Absolute Deviation
|
||||
|
||||
$$\text{Deviation}_t = |\text{Source}_t - \text{Middle}_{t-1}|$$
|
||||
|
||||
### 3. Average Absolute Deviation
|
||||
|
||||
$$\text{AvgDev}_t = \frac{1}{n} \sum_{i=0}^{n-1} \text{Deviation}_{t-i}$$
|
||||
|
||||
### 4. Band Calculation
|
||||
|
||||
$$\text{Upper}_t = \text{Middle}_t + (k \times \text{AvgDev}_t)$$
|
||||
|
||||
$$\text{Lower}_t = \text{Middle}_t - (k \times \text{AvgDev}_t)$$
|
||||
|
||||
Where $n$ = lookback period (default: 20), $k$ = multiplier (default: 2.0).
|
||||
|
||||
```csharp
|
||||
// Streaming usage
|
||||
var abber = new Abber(period: 20, multiplier: 2.0);
|
||||
foreach (var price in priceData)
|
||||
{
|
||||
abber.Update(price);
|
||||
// Middle: abber.Last.Value, Upper: abber.Upper.Value, Lower: abber.Lower.Value
|
||||
}
|
||||
|
||||
// Batch calculation
|
||||
var (middle, upper, lower) = Abber.Batch(series, period: 20, multiplier: 2.0);
|
||||
|
||||
// Span-based (zero allocation)
|
||||
Abber.Batch(source.AsSpan(), middleOut.AsSpan(), upperOut.AsSpan(), lowerOut.AsSpan(), 20, 2.0);
|
||||
```
|
||||
|
||||
## Performance Profile
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **Throughput** | ~15 ns/bar | Running sums avoid recomputation |
|
||||
| **Allocations** | 0 | Zero heap allocations in streaming mode |
|
||||
| **Complexity** | O(1) streaming, O(n) batch | Constant time per bar via circular buffers |
|
||||
| **Accuracy** | 10 | Exact computation, no approximations |
|
||||
| **Timeliness** | 6 | Inherits SMA lag (period/2 bars typical) |
|
||||
| **Overshoot** | 3 | Resistant to outlier-induced band explosions |
|
||||
| **Smoothness** | 7 | Smoother than standard deviation under shock |
|
||||
|
||||
## Validation
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **TA-Lib** | N/A | Not implemented |
|
||||
| **Skender** | N/A | Not implemented |
|
||||
| **Tulip** | N/A | Not implemented |
|
||||
| **Ooples** | N/A | Not implemented |
|
||||
| **Internal** | ✅ | All API modes produce identical results |
|
||||
| **Manual Calc** | ✅ | Formula verification against known values |
|
||||
|
||||
ABBER lacks external library equivalents for cross-validation. Validation relies on internal consistency (streaming vs batch vs span) and manual calculation verification.
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
**Parameter sensitivity**: Multiplier of 2.0 captures ~89% of data under Gaussian assumptions, but market distributions vary. Adjust based on asset volatility characteristics.
|
||||
|
||||
**Lag inheritance**: ABBER inherits SMA lag. For a 20-period setting, expect approximately 10 bars of delay in band response. Not suitable for high-frequency mean reversion where milliseconds matter.
|
||||
|
||||
**Band width interpretation**: Narrowing bands signal consolidation, but ABBER narrows more slowly than Bollinger Bands after volatility spikes. The "squeeze" pattern requires recalibration when switching from standard deviation to absolute deviation.
|
||||
@@ -0,0 +1,47 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Aberration (ABBER)", "ABBER", overlay=true)
|
||||
|
||||
//@function Calculates Aberration bands measuring deviation from a central moving average
|
||||
//@param source Series to calculate aberration from
|
||||
//@param ma_line Pre-calculated moving average line
|
||||
//@param period Lookback period for deviation calculation
|
||||
//@param multiplier Multiplier for deviation bands
|
||||
//@returns [upper_band, lower_band, deviation] Aberration band values and deviation
|
||||
//@optimized Uses simple deviation averaging with O(n) complexity
|
||||
abber(series float source, series float ma_line, simple int period, simple float multiplier) =>
|
||||
if period <= 0 or multiplier <= 0.0
|
||||
runtime.error("Period and multiplier must be greater than 0")
|
||||
float deviation = math.abs(nz(source) - nz(ma_line))
|
||||
float avg_deviation = ta.sma(deviation, period)
|
||||
float upper_band = ma_line + multiplier * avg_deviation
|
||||
float lower_band = ma_line - multiplier * avg_deviation
|
||||
[upper_band, lower_band, avg_deviation]
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_source = input.source(close, "Source")
|
||||
i_period = input.int(20, "Period", minval=1)
|
||||
i_ma_type = input.string("SMA", "Moving Average Type", options=["SMA", "EMA", "WMA", "RMA", "HMA"])
|
||||
i_multiplier = input.float(2.0, "Deviation Multiplier", minval=0.1, step=0.1)
|
||||
i_show_ma = input.bool(true, "Show Moving Average Line")
|
||||
|
||||
// Calculate the moving average based on selected type
|
||||
ma_line = switch i_ma_type
|
||||
"SMA" => ta.sma(i_source, i_period)
|
||||
"EMA" => ta.ema(i_source, i_period)
|
||||
"WMA" => ta.wma(i_source, i_period)
|
||||
"RMA" => ta.rma(i_source, i_period)
|
||||
"HMA" => ta.wma(2 * ta.wma(i_source, i_period / 2) - ta.wma(i_source, i_period), math.round(math.sqrt(i_period)))
|
||||
=> ta.sma(i_source, i_period)
|
||||
|
||||
// Calculation
|
||||
[upper_band, lower_band, deviation] = abber(i_source, ma_line, i_period, i_multiplier)
|
||||
|
||||
// Plots
|
||||
p_upper = plot(upper_band, "Upper Band", color=color.yellow, linewidth=2)
|
||||
p_lower = plot(lower_band, "Lower Band", color=color.yellow, linewidth=2)
|
||||
plot(i_show_ma ? ma_line : na, "MA", color=color.yellow, linewidth=2)
|
||||
fill(p_upper, p_lower, color=color.new(color.blue, 90), title="Band Fill")
|
||||
Reference in New Issue
Block a user