python wrapper

This commit is contained in:
Miha Kralj
2026-02-28 14:14:35 -08:00
parent 82e0248eb0
commit 83e9511261
521 changed files with 62395 additions and 15669 deletions
+218
View File
@@ -0,0 +1,218 @@
using TradingPlatform.BusinessLayer;
using Xunit;
namespace QuanTAlib.Tests;
public class AberrQuantowerTests
{
[Fact]
public void Constructor_SetsDefaults()
{
var indicator = new AberrIndicator();
Assert.Equal(20, indicator.Period);
Assert.Equal(2.0, indicator.Multiplier);
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Equal("ABERR - Aberration Bands", indicator.Name);
Assert.False(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void MinHistoryDepths_MatchesPeriod()
{
var indicator = new AberrIndicator { Period = 25 };
Assert.Equal(25, indicator.MinHistoryDepths);
}
[Fact]
public void ShortName_IncludesParameters()
{
var indicator = new AberrIndicator { Period = 15, Multiplier = 1.5 };
Assert.Contains("15", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("1.5", indicator.ShortName, StringComparison.Ordinal);
}
[Fact]
public void Initialize_CreatesThreeLineSeries()
{
var indicator = new AberrIndicator { Period = 14 };
indicator.Initialize();
Assert.Equal(3, indicator.LinesSeries.Count);
Assert.Equal("Middle", indicator.LinesSeries[0].Name);
Assert.Equal("Upper", indicator.LinesSeries[1].Name);
Assert.Equal("Lower", indicator.LinesSeries[2].Name);
}
[Fact]
public void ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new AberrIndicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
Assert.Equal(1, indicator.LinesSeries[0].Count);
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
Assert.True(double.IsFinite(indicator.LinesSeries[1].GetValue(0)));
Assert.True(double.IsFinite(indicator.LinesSeries[2].GetValue(0)));
}
[Fact]
public void ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new AberrIndicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new AberrIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void ProcessUpdate_EmptyData_HandlesGracefully()
{
var indicator = new AberrIndicator { Period = 5 };
indicator.Initialize();
var args = new UpdateArgs(UpdateReason.NewBar);
var exception = Record.Exception(() => indicator.ProcessUpdate(args));
Assert.Null(exception);
}
[Fact]
public void MultipleUpdates_ProducesCorrectSequence()
{
var indicator = new AberrIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i);
indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
}
Assert.Equal(10, indicator.LinesSeries[0].Count);
Assert.Equal(10, indicator.LinesSeries[1].Count);
Assert.Equal(10, indicator.LinesSeries[2].Count);
// All values should be finite
for (int i = 0; i < 10; i++)
{
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(i)));
Assert.True(double.IsFinite(indicator.LinesSeries[1].GetValue(i)));
Assert.True(double.IsFinite(indicator.LinesSeries[2].GetValue(i)));
}
}
[Fact]
public void BandRelationship_UpperAboveLowerBelowMiddle()
{
var indicator = new AberrIndicator { Period = 5, Multiplier = 2.0 };
indicator.Initialize();
var now = DateTime.UtcNow;
// Use varying prices to create volatility
var prices = new[] { 100, 105, 98, 110, 95, 115, 92, 118, 90, 120 };
for (int i = 0; i < prices.Length; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), prices[i], prices[i] + 5, prices[i] - 3, prices[i] + 2, 1000);
indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
}
// After warmup, upper >= middle >= lower (when there is volatility)
double middle = indicator.LinesSeries[0].GetValue(0);
double upper = indicator.LinesSeries[1].GetValue(0);
double lower = indicator.LinesSeries[2].GetValue(0);
Assert.True(upper >= middle, $"Upper ({upper}) should be >= Middle ({middle})");
Assert.True(lower <= middle, $"Lower ({lower}) should be <= Middle ({middle})");
}
[Fact]
public void Multiplier_AffectsBandWidth()
{
var now = DateTime.UtcNow;
// Use varying prices to create volatility
var prices = new[] { 100, 105, 98, 110, 95, 115, 92, 118, 90, 120 };
// Narrow bands with multiplier 1.0
var narrowIndicator = new AberrIndicator { Period = 5, Multiplier = 1.0 };
narrowIndicator.Initialize();
// Wide bands with multiplier 3.0
var wideIndicator = new AberrIndicator { Period = 5, Multiplier = 3.0 };
wideIndicator.Initialize();
for (int i = 0; i < prices.Length; i++)
{
narrowIndicator.HistoricalData.AddBar(now.AddMinutes(i), prices[i], prices[i] + 5, prices[i] - 3, prices[i] + 2, 1000);
narrowIndicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
wideIndicator.HistoricalData.AddBar(now.AddMinutes(i), prices[i], prices[i] + 5, prices[i] - 3, prices[i] + 2, 1000);
wideIndicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
}
double narrowWidth = narrowIndicator.LinesSeries[1].GetValue(0) - narrowIndicator.LinesSeries[2].GetValue(0);
double wideWidth = wideIndicator.LinesSeries[1].GetValue(0) - wideIndicator.LinesSeries[2].GetValue(0);
Assert.True(wideWidth > narrowWidth, $"Wide bands ({wideWidth}) should be wider than narrow bands ({narrowWidth})");
}
[Fact]
public void SourceType_CanBeChanged()
{
var indicator = new AberrIndicator { Source = SourceType.Close };
Assert.Equal(SourceType.Close, indicator.Source);
indicator.Source = SourceType.HLC3;
Assert.Equal(SourceType.HLC3, indicator.Source);
}
[Fact]
public void ShowColdValues_CanBeToggled()
{
var indicator = new AberrIndicator { ShowColdValues = true };
Assert.True(indicator.ShowColdValues);
indicator.ShowColdValues = false;
Assert.False(indicator.ShowColdValues);
}
[Fact]
public void SourceCodeLink_IsValid()
{
var indicator = new AberrIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Aberr.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
}
+92
View File
@@ -0,0 +1,92 @@
using System.Drawing;
using System.Runtime.CompilerServices;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
/// <summary>
/// ABERR (Aberration Bands) - Volatility bands using absolute deviation
/// A Quantower indicator adapter that provides three bands based on mean absolute deviation
/// rather than standard deviation, making it more robust to outliers than Bollinger Bands.
/// </summary>
[SkipLocalsInit]
public sealed class AberrIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
public int Period { get; set; } = 20;
[InputParameter("Multiplier", sortIndex: 2, 0.1, 10.0, 0.1, 2)]
public double Multiplier { get; set; } = 2.0;
[InputParameter("Data source", sortIndex: 3, variants: [
"Open", SourceType.Open,
"High", SourceType.High,
"Low", SourceType.Low,
"Close", SourceType.Close,
"HL/2 (Median)", SourceType.HL2,
"Midbody (O+C)/2", SourceType.Midbody,
"OHL/3 (Mean)", SourceType.OHL3,
"HLC/3 (Typical)", SourceType.HLC3,
"OHLC/4 (Average)", SourceType.OHLC4,
"HLCC/4 (Weighted)", SourceType.HLCC4
])]
public SourceType Source { get; set; } = SourceType.Close;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Aberr? _aberr;
private Func<IHistoryItem, double>? _selector;
private readonly LineSeries _middleSeries;
private readonly LineSeries _upperSeries;
private readonly LineSeries _lowerSeries;
public int MinHistoryDepths => Period;
public override string ShortName => $"ABERR {Period},{Multiplier:F1}";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/channels/aberr/Aberr.Quantower.cs";
public AberrIndicator()
{
OnBackGround = true;
SeparateWindow = false;
Name = "ABERR - Aberration Bands";
Description = "Volatility bands using absolute deviation (robust to outliers)";
_middleSeries = new LineSeries(name: "Middle", color: Color.FromArgb(255, 128, 128), width: 2, style: LineStyle.Solid);
_upperSeries = new LineSeries(name: "Upper", color: Color.FromArgb(255, 160, 160), width: 1, style: LineStyle.Dash);
_lowerSeries = new LineSeries(name: "Lower", color: Color.FromArgb(255, 160, 160), width: 1, style: LineStyle.Dash);
AddLineSeries(_middleSeries);
AddLineSeries(_upperSeries);
AddLineSeries(_lowerSeries);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnInit()
{
_aberr = new Aberr(Period, Multiplier);
_selector = Source.GetPriceSelector();
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
if (HistoricalData.Count == 0 || _aberr is null || _selector is null)
{
return;
}
var item = HistoricalData[0, SeekOriginHistory.End];
double value = _selector(item);
TValue input = new(item.TimeLeft, value);
_aberr.Update(input, args.IsNewBar());
_middleSeries.SetValue(_aberr.Last.Value, _aberr.IsHot, ShowColdValues);
_upperSeries.SetValue(_aberr.Upper.Value, _aberr.IsHot, ShowColdValues);
_lowerSeries.SetValue(_aberr.Lower.Value, _aberr.IsHot, ShowColdValues);
}
}
+674
View File
@@ -0,0 +1,674 @@
namespace QuanTAlib.Tests;
public class AberrTests
{
[Fact]
public void Aberr_Constructor_ValidatesInput()
{
// Period validation
Assert.Throws<ArgumentException>(() => new Aberr(0));
Assert.Throws<ArgumentException>(() => new Aberr(-1));
// Multiplier validation
Assert.Throws<ArgumentException>(() => new Aberr(10, 0));
Assert.Throws<ArgumentException>(() => new Aberr(10, -1));
// Valid construction
var aberr = new Aberr(10);
Assert.NotNull(aberr);
var aberr2 = new Aberr(20, 3.0);
Assert.NotNull(aberr2);
}
[Fact]
public void Aberr_Calc_ReturnsValue()
{
var aberr = new Aberr(10);
Assert.Equal(0, aberr.Last.Value);
Assert.Equal(0, aberr.Upper.Value);
Assert.Equal(0, aberr.Lower.Value);
TValue result = aberr.Update(new TValue(DateTime.UtcNow, 100));
Assert.True(double.IsFinite(result.Value));
Assert.Equal(result.Value, aberr.Last.Value);
Assert.True(double.IsFinite(aberr.Upper.Value));
Assert.True(double.IsFinite(aberr.Lower.Value));
}
[Fact]
public void Aberr_FirstValue_ReturnsExpected()
{
var aberr = new Aberr(10);
// First value: source = 100
// SMA(1) = 100, Deviation = |100 - 100| = 0, AvgDeviation = 0
// Middle = 100, Upper = 100 + 0 = 100, Lower = 100 - 0 = 100
aberr.Update(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100.0, aberr.Last.Value, 1e-10);
Assert.Equal(100.0, aberr.Upper.Value, 1e-10);
Assert.Equal(100.0, aberr.Lower.Value, 1e-10);
}
[Fact]
public void Aberr_Calc_IsNew_AcceptsParameter()
{
var aberr = new Aberr(10);
aberr.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
double value1 = aberr.Last.Value;
aberr.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
double value2 = aberr.Last.Value;
// Values should change with new data
Assert.NotEqual(value1, value2);
}
[Fact]
public void Aberr_Calc_IsNew_False_UpdatesValue()
{
var aberr = new Aberr(10);
aberr.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
aberr.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
double beforeUpdate = aberr.Last.Value;
aberr.Update(new TValue(DateTime.UtcNow, 120), isNew: false);
double afterUpdate = aberr.Last.Value;
// Update should change the value
Assert.NotEqual(beforeUpdate, afterUpdate);
}
[Fact]
public void Aberr_Reset_ClearsState()
{
var aberr = new Aberr(10);
aberr.Update(new TValue(DateTime.UtcNow, 100));
aberr.Update(new TValue(DateTime.UtcNow, 105));
double middleBefore = aberr.Last.Value;
aberr.Reset();
Assert.Equal(0, aberr.Last.Value);
Assert.Equal(0, aberr.Upper.Value);
Assert.Equal(0, aberr.Lower.Value);
Assert.False(aberr.IsHot);
// After reset, should accept new values
aberr.Update(new TValue(DateTime.UtcNow, 50));
Assert.NotEqual(0, aberr.Last.Value);
Assert.NotEqual(middleBefore, aberr.Last.Value);
}
[Fact]
public void Aberr_Properties_Accessible()
{
var aberr = new Aberr(10, 2.5);
Assert.Equal(0, aberr.Last.Value);
Assert.False(aberr.IsHot);
Assert.Contains("Aberr", aberr.Name, StringComparison.Ordinal);
Assert.Equal(10, aberr.WarmupPeriod);
aberr.Update(new TValue(DateTime.UtcNow, 100));
Assert.NotEqual(0, aberr.Last.Value);
}
[Fact]
public void Aberr_IsHot_BecomesTrueWhenBufferFull()
{
var aberr = new Aberr(5);
Assert.False(aberr.IsHot);
for (int i = 1; i <= 4; i++)
{
aberr.Update(new TValue(DateTime.UtcNow, 100 + i));
Assert.False(aberr.IsHot);
}
aberr.Update(new TValue(DateTime.UtcNow, 105));
Assert.True(aberr.IsHot);
}
[Fact]
public void Aberr_CalculatesCorrectBands()
{
var aberr = new Aberr(3, 2.0);
// Bar 1: source = 100
// SMA = 100, Deviation = |100-100| = 0, AvgDev = 0
aberr.Update(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100.0, aberr.Last.Value, 1e-10);
// Bar 2: source = 110
// SMA(2) = (100+110)/2 = 105
// Dev1 = 0, Dev2 = |110 - 105| = 5 (same-bar SMA)
// AvgDev = (0+5)/2 = 2.5
// Upper = 105 + 2*2.5 = 110, Lower = 105 - 2*2.5 = 100
aberr.Update(new TValue(DateTime.UtcNow, 110));
Assert.Equal(105.0, aberr.Last.Value, 1e-10);
// Bar 3: source = 120
// SMA(3) = (100+110+120)/3 = 110
// Dev3 = |120 - 110| = 10 (same-bar SMA)
// AvgDev = (0+5+10)/3 = 5.0
// Upper = 110 + 2*5 = 120, Lower = 110 - 2*5 = 100
aberr.Update(new TValue(DateTime.UtcNow, 120));
Assert.Equal(110.0, aberr.Last.Value, 1e-10);
Assert.Equal(120.0, aberr.Upper.Value, 1e-10);
Assert.Equal(100.0, aberr.Lower.Value, 1e-10);
}
[Fact]
public void Aberr_SlidingWindow_Works()
{
var aberr = new Aberr(3, 2.0);
// Feed initial values
aberr.Update(new TValue(DateTime.UtcNow, 100));
aberr.Update(new TValue(DateTime.UtcNow, 110));
aberr.Update(new TValue(DateTime.UtcNow, 120));
double middle1 = aberr.Last.Value;
// Add another value - window slides
aberr.Update(new TValue(DateTime.UtcNow, 130));
// SMA(3) should now be (110+120+130)/3 = 120
Assert.NotEqual(middle1, aberr.Last.Value);
Assert.Equal(120.0, aberr.Last.Value, 1e-10);
}
[Fact]
public void Aberr_IterativeCorrections_RestoreToOriginalState()
{
var aberr = new Aberr(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);
aberr.Update(tenthInput, isNew: true);
}
// Remember state after 10 values
double middleAfterTen = aberr.Last.Value;
double upperAfterTen = aberr.Upper.Value;
double lowerAfterTen = aberr.Lower.Value;
// Generate 9 corrections with isNew=false (different values)
for (int i = 0; i < 9; i++)
{
var bar = gbm.Next(isNew: false);
aberr.Update(new TValue(bar.Time, bar.Close), isNew: false);
}
// Feed the remembered 10th input again with isNew=false
aberr.Update(tenthInput, isNew: false);
// State should match the original state after 10 values
Assert.Equal(middleAfterTen, aberr.Last.Value, 1e-10);
Assert.Equal(upperAfterTen, aberr.Upper.Value, 1e-10);
Assert.Equal(lowerAfterTen, aberr.Lower.Value, 1e-10);
}
[Fact]
public void Aberr_BatchCalc_MatchesIterativeCalc()
{
var aberrIterative = new Aberr(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)
{
aberrIterative.Update(item);
iterativeMiddle.Add(aberrIterative.Last.Value);
iterativeUpper.Add(aberrIterative.Upper.Value);
iterativeLower.Add(aberrIterative.Lower.Value);
}
// Calculate batch
var aberrBatch = new Aberr(10);
var (batchMiddle, batchUpper, batchLower) = aberrBatch.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 Aberr_NaN_Input_UsesLastValidValue()
{
var aberr = new Aberr(5);
// Feed some valid values
aberr.Update(new TValue(DateTime.UtcNow, 100));
aberr.Update(new TValue(DateTime.UtcNow, 105));
// Feed NaN - should use last valid value
var resultAfterNaN = aberr.Update(new TValue(DateTime.UtcNow, double.NaN));
// Result should be finite (not NaN)
Assert.True(double.IsFinite(resultAfterNaN.Value));
Assert.True(double.IsFinite(aberr.Upper.Value));
Assert.True(double.IsFinite(aberr.Lower.Value));
}
[Fact]
public void Aberr_Infinity_Input_UsesLastValidValue()
{
var aberr = new Aberr(5);
// Feed some valid values
aberr.Update(new TValue(DateTime.UtcNow, 100));
aberr.Update(new TValue(DateTime.UtcNow, 105));
// Feed positive infinity
var resultAfterPosInf = aberr.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
Assert.True(double.IsFinite(resultAfterPosInf.Value));
Assert.True(double.IsFinite(aberr.Upper.Value));
Assert.True(double.IsFinite(aberr.Lower.Value));
// Feed negative infinity
var resultAfterNegInf = aberr.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
Assert.True(double.IsFinite(resultAfterNegInf.Value));
Assert.True(double.IsFinite(aberr.Upper.Value));
Assert.True(double.IsFinite(aberr.Lower.Value));
}
[Fact]
public void Aberr_MultipleNaN_ContinuesWithLastValid()
{
var aberr = new Aberr(5);
// Feed valid values
aberr.Update(new TValue(DateTime.UtcNow, 100));
aberr.Update(new TValue(DateTime.UtcNow, 105));
aberr.Update(new TValue(DateTime.UtcNow, 110));
// Feed multiple NaN values
var r1 = aberr.Update(new TValue(DateTime.UtcNow, double.NaN));
var r2 = aberr.Update(new TValue(DateTime.UtcNow, double.NaN));
var r3 = aberr.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 Aberr_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) = Aberr.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 Aberr_Period1_ReturnsDirectCalculation()
{
var aberr = new Aberr(1);
// Single value: SMA(1) = 100, Deviation = 0
aberr.Update(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100.0, aberr.Last.Value, 1e-10);
Assert.Equal(100.0, aberr.Upper.Value, 1e-10);
Assert.Equal(100.0, aberr.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
aberr.Update(new TValue(DateTime.UtcNow, 110));
Assert.Equal(110.0, aberr.Last.Value, 1e-10);
}
// ============== Span API Tests ==============
[Fact]
public void Aberr_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>(() =>
Aberr.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 0));
Assert.Throws<ArgumentException>(() =>
Aberr.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), -1));
// Multiplier must be > 0
Assert.Throws<ArgumentException>(() =>
Aberr.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 3, 0));
Assert.Throws<ArgumentException>(() =>
Aberr.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>(() =>
Aberr.Batch(source.AsSpan(), shortOutput.AsSpan(), upper.AsSpan(), lower.AsSpan(), 3));
}
[Fact]
public void Aberr_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) = Aberr.Batch(series, 10);
// Calculate with Span API
double[] spanMiddle = new double[100];
double[] spanUpper = new double[100];
double[] spanLower = new double[100];
Aberr.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 Aberr_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
Aberr.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 Aberr_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];
Aberr.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 Aberr_AllModes_ProduceSameResult()
{
// Arrange
const 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) = Aberr.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];
Aberr.Batch(source.AsSpan(), spanMiddle.AsSpan(), spanUpper.AsSpan(), spanLower.AsSpan(), period, multiplier);
// 3. Streaming Mode
var streamingInd = new Aberr(period, multiplier);
foreach (var item in series)
{
streamingInd.Update(item);
}
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 Aberr(pubSource, period, multiplier);
foreach (var item in series)
{
pubSource.Add(item);
}
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 Aberr_Chainability_Works()
{
var source = new TSeries();
var aberr = new Aberr(source, 10);
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, aberr.Last.Value);
}
[Fact]
public void Aberr_WarmupPeriod_IsSetCorrectly()
{
var aberr = new Aberr(10);
Assert.Equal(10, aberr.WarmupPeriod);
}
[Fact]
public void Aberr_Prime_SetsStateCorrectly()
{
var aberr = new Aberr(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);
aberr.Prime(series);
Assert.True(aberr.IsHot);
// Last 3 values: 120, 130, 140 -> SMA = 130
Assert.Equal(130.0, aberr.Last.Value, 1e-10);
// Verify it continues correctly
aberr.Update(new TValue(DateTime.UtcNow, 150));
// New window: 130, 140, 150 -> SMA = 140
Assert.Equal(140.0, aberr.Last.Value, 1e-10);
}
[Fact]
public void Aberr_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) = Aberr.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 Aberr_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, _) = Aberr.Batch(series, 5, 1.0);
// Multiplier 3.0
var (middle3, upper3, _) = Aberr.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 Aberr_FlatLine_ReturnsSameValues()
{
var aberr = new Aberr(10);
for (int i = 0; i < 20; i++)
{
aberr.Update(new TValue(DateTime.UtcNow, 100));
}
// When all values are the same, SMA = 100, all deviations = 0
Assert.Equal(100.0, aberr.Last.Value, 1e-10);
Assert.Equal(100.0, aberr.Upper.Value, 1e-10);
Assert.Equal(100.0, aberr.Lower.Value, 1e-10);
}
[Fact]
public void Aberr_Pub_EventFires()
{
var aberr = new Aberr(10);
bool eventFired = false;
aberr.Pub += (object? _, in TValueEventArgs _) => eventFired = true;
aberr.Update(new TValue(DateTime.UtcNow, 100));
Assert.True(eventFired);
}
[Fact]
public void Aberr_BandsAreSymmetric()
{
var aberr = new Aberr(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);
aberr.Update(new TValue(bar.Time, bar.Close));
}
// Upper - Middle should equal Middle - Lower
double upperDiff = aberr.Upper.Value - aberr.Last.Value;
double lowerDiff = aberr.Last.Value - aberr.Lower.Value;
Assert.Equal(upperDiff, lowerDiff, 1e-10);
}
}
@@ -0,0 +1,424 @@
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
/// <summary>
/// Validation tests for Aberr indicator.
/// Note: Skender.Stock.Indicators, TA-Lib, Tulip, and OoplesFinance do not provide
/// Aberr (Aberration Bands) implementation for cross-validation. These tests validate
/// against manual calculations and internal consistency across all API modes.
/// </summary>
public sealed class AberrValidationTests(ITestOutputHelper output) : IDisposable
{
private readonly ValidationTestData _testData = new();
private bool _disposed;
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 (same-bar SMA deviation)
// Values: [100, 110, 120]
// Bar 1: SMA=100, Dev=|100-100|=0, AvgDev=0
// Bar 2: SMA=(100+110)/2=105, Dev2=|110-105|=5, AvgDev=(0+5)/2=2.5
// Bar 3: SMA=(100+110+120)/3=110, Dev3=|120-110|=10, AvgDev=(0+5+10)/3=5.0
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 aberr = new Aberr(3, 2.0);
var (middle, upper, lower) = aberr.Update(series);
// SMA(3) = 110
Assert.Equal(110.0, middle.Last.Value, 1e-10);
// AvgDev = (0 + 5 + 10) / 3 = 5.0
const double expectedAvgDev = 5.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("Aberr 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 aberr = new Aberr(5, 2.0);
var (middle, _, _) = aberr.Update(series);
// SMA(5) = (100 + 110 + 120 + 130 + 140) / 5 = 120
Assert.Equal(120.0, middle.Last.Value, 1e-10);
output.WriteLine("Aberr 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, _) = Aberr.Batch(series, 10, 1.0);
var (middle2, upper2, _) = Aberr.Batch(series, 10, 2.0);
var (middle3, upper3, _) = Aberr.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("Aberr 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 aberr = new Aberr(period, 2.0);
var (qMiddle, qUpper, qLower) = aberr.Update(_testData.Data);
// Static batch
var (sMiddle, sUpper, sLower) = Aberr.Batch(_testData.Data, period, 2.0);
// Verify match
ValidationHelper.VerifySeriesEqual(qMiddle, sMiddle);
ValidationHelper.VerifySeriesEqual(qUpper, sUpper);
ValidationHelper.VerifySeriesEqual(qLower, sLower);
}
output.WriteLine("Aberr 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 streamingAberr = new Aberr(period, 2.0);
var streamMiddle = new TSeries();
var streamUpper = new TSeries();
var streamLower = new TSeries();
foreach (var item in _testData.Data)
{
streamingAberr.Update(item);
streamMiddle.Add(streamingAberr.Last);
streamUpper.Add(streamingAberr.Upper);
streamLower.Add(streamingAberr.Lower);
}
// Batch mode for comparison
var (batchMiddle, batchUpper, batchLower) = Aberr.Batch(_testData.Data, period, 2.0);
// Verify match
ValidationHelper.VerifySeriesEqual(batchMiddle, streamMiddle);
ValidationHelper.VerifySeriesEqual(batchUpper, streamUpper);
ValidationHelper.VerifySeriesEqual(batchLower, streamLower);
}
output.WriteLine("Aberr 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];
Aberr.Batch(source.AsSpan(), spanMiddle.AsSpan(), spanUpper.AsSpan(), spanLower.AsSpan(),
period, 2.0);
// Batch mode for comparison
var (batchMiddle, batchUpper, batchLower) = Aberr.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("Aberr 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 Aberr(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) = Aberr.Batch(_testData.Data, period, 2.0);
// Verify match
ValidationHelper.VerifySeriesEqual(batchMiddle, eventMiddle);
ValidationHelper.VerifySeriesEqual(batchUpper, eventUpper);
ValidationHelper.VerifySeriesEqual(batchLower, eventLower);
}
output.WriteLine("Aberr 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) = Aberr.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("Aberr Calculate method validated successfully");
}
[Fact]
public void Validate_LargeDataset_NoOverflow()
{
// Test with the full 5000 bar dataset
var (middle, upper, lower) = Aberr.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("Aberr 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) = Aberr.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("Aberr 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) = Aberr.Batch(_testData.Data, period, 2.0);
// Prime indicator with subset and continue
var primedIndicator = new Aberr(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("Aberr Prime method validated successfully");
}
[Fact]
public void Validate_MiddleBand_MatchesSMA()
{
// Verify the middle band is exactly the SMA
int period = 20;
var aberr = new Aberr(period, 2.0);
var sma = new Sma(period);
var aberrResults = aberr.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, aberrResults.Middle[i].Value, 1e-10);
}
output.WriteLine("Aberr 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 aberr = new Aberr(period, 1.0); // multiplier = 1 for easier verification
var (middle, upper, _) = aberr.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("Aberr 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) = Aberr.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($"Aberr consistency across {periods.Length} periods validated successfully");
}
}
+705
View File
@@ -0,0 +1,705 @@
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// Aberr: 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/aberr.pine
/// </remarks>
[SkipLocalsInit]
public sealed class Aberr : ITValuePublisher, IDisposable
{
private readonly int _period;
private readonly double _multiplier;
private readonly RingBuffer _sourceBuffer;
private readonly RingBuffer _deviationBuffer;
private readonly TValuePublishedHandler _handler;
private ITValuePublisher? _source;
private bool _disposed;
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 _pState;
/// <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 Aberr 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 Aberr(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 = $"Aberr({period},{multiplier:F2})";
WarmupPeriod = period;
_handler = HandleValue;
}
/// <summary>
/// Creates Aberr with TSeries source.
/// </summary>
public Aberr(TSeries source, int period, double multiplier = 2.0) : this(period, multiplier)
{
_source = source ?? throw new ArgumentNullException(nameof(source));
Prime(source);
_source.Pub += _handler;
}
/// <summary>
/// Creates Aberr with ITValuePublisher source.
/// </summary>
public Aberr(ITValuePublisher source, int period, double multiplier = 2.0) : this(period, multiplier)
{
_source = source ?? throw new ArgumentNullException(nameof(source));
_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)
{
_pState = _state;
// Compute SMA including the new value with correct divisor (matches batch ProcessMainLoop)
int count = _sourceBuffer.Count;
double removedSource = count == _sourceBuffer.Capacity ? _sourceBuffer.Oldest : 0.0;
double newSum = _state.SumSource - removedSource + value;
int newCount = count < _sourceBuffer.Capacity ? count + 1 : count;
double sma = newSum / newCount;
double deviation = Math.Abs(value - sma);
UpdateState(value, deviation);
}
else
{
_state = _pState;
// Replace newest source value and recompute sum for current-bar SMA (matches Pine)
_sourceBuffer.UpdateNewest(value);
double currentSum = _sourceBuffer.Sum;
int corrCount = _sourceBuffer.Count;
double corrSma = corrCount > 0 ? currentSum / corrCount : value;
double corrDeviation = Math.Abs(value - corrSma);
_deviationBuffer.UpdateNewest(corrDeviation);
_state = _state with
{
SumSource = currentSum,
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));
}
[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)
{
ReadOnlySpan<double> sourceSpan = buffers.Source;
ReadOnlySpan<double> deviationSpan = buffers.Deviation;
state.SumSource = sourceSpan.SumSIMD();
state.SumDeviation = deviationSpan.SumSIMD();
}
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;
}
}
/// <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;
_pState = 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);
// Compute SMA including the new value with correct divisor (matches batch ProcessMainLoop)
int count = _sourceBuffer.Count;
double removedSource = count == _sourceBuffer.Capacity ? _sourceBuffer.Oldest : 0.0;
double newSum = _state.SumSource - removedSource + value;
int newCount = count < _sourceBuffer.Capacity ? count + 1 : count;
double sma = newSum / newCount;
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);
}
_pState = _state;
}
/// <summary>
/// Resets the indicator state.
/// </summary>
public void Reset()
{
_sourceBuffer.Clear();
_deviationBuffer.Clear();
_state = default;
_pState = default;
Last = default;
Upper = default;
Lower = default;
}
/////////////////////////////////////////////////////////////////////////////////////////////////
// Static Batch Methods
/////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary>
/// Output buffers for batch Aberr 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 BufferIndex;
public int TickCount;
}
/// <summary>
/// Working buffers for batch calculation.
/// </summary>
[StructLayout(LayoutKind.Auto)]
private readonly ref struct WorkBuffers(Span<double> source, Span<double> deviation)
{
public readonly Span<double> Source = source;
public readonly Span<double> Deviation = deviation;
}
/// <summary>
/// Calculates Aberr 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 aberr = new Aberr(period, multiplier);
return aberr.Update(source);
}
/// <summary>
/// Calculates Aberr 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 Aberr 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);
// Compute SMA including the new value to get same-bar deviation (matches Pine)
int newCount = i + 1;
double newSum = state.SumSource + v;
double sma = newSum / newCount;
double deviation = Math.Abs(v - sma);
state.SumSource = newSum;
state.SumDeviation += deviation;
buffers.Source[i] = v;
buffers.Deviation[i] = deviation;
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);
// Compute SMA including the new value to get same-bar deviation (matches Pine)
double newSumSource = state.SumSource - buffers.Source[state.BufferIndex] + v;
double sma = newSumSource / period;
double deviation = Math.Abs(v - sma);
// Update running sums using single buffer index
state.SumSource = newSumSource;
buffers.Source[state.BufferIndex] = v;
state.SumDeviation = state.SumDeviation - buffers.Deviation[state.BufferIndex] + deviation;
buffers.Deviation[state.BufferIndex] = deviation;
state.BufferIndex++;
if (state.BufferIndex >= period)
{
state.BufferIndex = 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);
}
}
}
/// <summary>
/// Runs a high-performance batch calculation and returns a "Hot" Aberr instance.
/// </summary>
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Aberr Indicator) Calculate(TSeries source, int period, double multiplier = 2.0)
{
var aberr = new Aberr(period, multiplier);
var results = aberr.Update(source);
return (results, aberr);
}
/// <summary>
/// Disposes the Aberr instance, unsubscribing from the source publisher.
/// This method is idempotent.
/// </summary>
public void Dispose()
{
if (!_disposed)
{
if (_source != null)
{
_source.Pub -= _handler;
_source = null;
}
_disposed = true;
}
}
}
+135
View File
@@ -0,0 +1,135 @@
# ABERR: Aberration Bands
| Property | Value |
| ---------------- | -------------------------------- |
| **Category** | Channel |
| **Inputs** | Source (close) |
| **Parameters** | `period`, `multiplier` (default 2.0) |
| **Outputs** | Multiple series (Upper, Lower) |
| **Output range** | Tracks input |
| **Warmup** | `period` bars |
### TL;DR
- ABERR measures price deviation from a central moving average using mean absolute deviation rather than standard deviation, producing dynamic bands ...
- Parameterized by `period`, `multiplier` (default 2.0).
- Output range: Tracks input.
- Requires `period` bars of warmup before first valid output (IsHot = true).
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
ABERR measures price deviation from a central moving average using mean absolute deviation rather than standard deviation, producing dynamic bands that adapt to volatility while remaining robust against extreme outliers. Where Bollinger Bands amplify outliers through squaring (the $L^2$ norm), ABERR uses raw absolute differences (the $L^1$ norm), so bands respond to typical price behavior rather than the occasional spike that yanks everything sideways. For a 20-period window with a 2.0 multiplier, ABERR contains approximately 89% of normally-distributed price action, but its real advantage emerges with fat-tailed distributions where standard deviation overreacts to single-bar anomalies.
## Historical Context
The absolute deviation approach predates Bollinger's work by decades. Mean absolute deviation appears in early 20th-century statistics as a robust alternative to standard deviation, championed by statisticians who recognized that squaring deviations gives disproportionate weight to outliers. In financial markets, applying absolute deviation to band construction arrived after practitioners grew tired of watching Bollinger Bands blow out on single-bar anomalies such as flash crashes, earnings gaps, and fat-finger trades.
No single inventor claims credit for ABERR. The technique spread through trading floors where robustness mattered more than textbook elegance. The mathematical distinction is fundamental: standard deviation is a quadratic spring that amplifies outliers, while mean absolute deviation is a linear damper that treats all deviations proportionally. Under Gaussian assumptions, $\text{MAD} \approx 0.7979 \sigma$, so ABERR with multiplier 2.0 is roughly equivalent to Bollinger Bands with multiplier 1.6. But on real market data with kurtosis > 3, the gap widens in ABERR's favor.
## Architecture & Physics
### 1. Central Tendency (SMA)
The middle band is a Simple Moving Average over the lookback window:
$$\text{Middle}_t = \frac{1}{n} \sum_{i=0}^{n-1} x_{t-i}$$
### 2. Absolute Deviation
Each bar's deviation is measured against the previous middle band value:
$$d_t = |x_t - \text{Middle}_{t-1}|$$
### 3. Average Absolute Deviation
The deviation series is itself averaged over the same window:
$$\text{AvgDev}_t = \frac{1}{n} \sum_{i=0}^{n-1} d_{t-i}$$
### 4. Band Construction
$$\text{Upper}_t = \text{Middle}_t + k \cdot \text{AvgDev}_t$$
$$\text{Lower}_t = \text{Middle}_t - k \cdot \text{AvgDev}_t$$
### 5. Complexity
Both the SMA and the average deviation use circular buffers with running sums, yielding $O(1)$ per bar in streaming mode. The SIMD-accelerable portion is the final band construction step ($\text{Middle} \pm k \cdot \text{AvgDev}$), while the running-sum maintenance is inherently serial.
## Mathematical Foundation
### Parameters
| Parameter | Description | Default | Constraint |
|-----------|-------------|---------|------------|
| `period` | Lookback window for SMA and deviation averaging | 20 | $> 0$ |
| `multiplier` | Band width scale factor ($k$) | 2.0 | $> 0$ |
| `source` | Input price series | close | |
| `ma_line` | Pre-computed moving average (center line) | SMA | configurable |
### Relationship to Standard Deviation
For a normal distribution:
$$\text{MAD} = \sigma \sqrt{\frac{2}{\pi}} \approx 0.7979\,\sigma$$
Therefore ABERR with $k = 2.0$ captures approximately the same range as Bollinger Bands with $k \approx 1.596$.
### Pseudo-code
```
function ABERR(source, ma_line, period, multiplier):
// Deviation from center line
deviation = |source - ma_line|
// Average absolute deviation (SMA of deviations)
avg_dev = SMA(deviation, period)
// Band construction
upper = ma_line + multiplier * avg_dev
lower = ma_line - multiplier * avg_dev
return [upper, lower, avg_dev]
```
### Output Interpretation
| Output | Description |
|--------|-------------|
| `upper` | Upper aberration band |
| `lower` | Lower aberration band |
| `avg_dev` | Current average absolute deviation (band half-width before scaling) |
## Performance Profile
### Operation Count (Streaming Mode)
ABERR maintains two running-sum ring buffers (SMA of price and SMA of absolute deviations), each updated in $O(1)$:
| Operation | Count | Cost (cycles) | Subtotal |
| :--- | :---: | :---: | :---: |
| SUB (oldest from running sum) | 2 | 1 | 2 |
| ADD (new value to running sum) | 2 | 1 | 2 |
| DIV (sum / count, two SMAs) | 2 | 15 | 30 |
| SUB (price - prevMiddle) | 1 | 1 | 1 |
| ABS (deviation) | 1 | 1 | 1 |
| MUL (multiplier × avgDev) | 1 | 3 | 3 |
| ADD/SUB (middle ± width) | 2 | 1 | 2 |
| **Total (hot)** | **11** | — | **~41 cycles** |
Warmup overhead is negligible: the ring buffer tracks count, adding one CMP per bar until full.
### Batch Mode (SIMD Analysis)
The running-sum SMA is inherently sequential (each bar depends on the previous running sum). SIMD parallelization across bars is not possible for the core SMA path:
| Optimization | Benefit |
| :--- | :--- |
| Band arithmetic (middle ± k × dev) | Vectorizable across output array with `Vector<double>` |
| ABS of deviations | Vectorizable with `Vector.Abs` for batch deviation pass |
| Running-sum maintenance | Sequential; cannot parallelize |
## Resources
- **Pham-Gia, T. & Hung, T.L.** "The Mean and Median Absolute Deviations." *Mathematical and Computer Modelling*, 34(7-8), 2001. (MAD vs. standard deviation theory)
- **Bollinger, J.** *Bollinger on Bollinger Bands*. McGraw-Hill, 2001. (Standard deviation band predecessor)
- **Hampel, F.R.** "The Influence Curve and its Role in Robust Estimation." *Journal of the American Statistical Association*, 69(346), 1974. (Robustness theory for $L^1$ vs $L^2$ norms)
+47
View File
@@ -0,0 +1,47 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Aberration (ABERR)", "ABERR", 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
aberr(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] = aberr(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")