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
+1 -1
View File
@@ -8,7 +8,7 @@ Channels define dynamic support and resistance. Upper band shows where price ten
| Indicator | Full Name | Description |
| :--- | :--- | :--- |
| [ABBER](abber/Abber.md) | Aberration Bands | Absolute deviation-based volatility bands. More robust than standard deviation. |
| [ABERR](aberr/Aberr.md) | Aberration Bands | Absolute deviation-based volatility bands. More robust than standard deviation. |
| [ACCBANDS](accbands/Accbands.md) | Acceleration Bands | Volatility-based adaptive channel by Price Headley. Width adapts to momentum. |
| [APCHANNEL](apchannel/Apchannel.md) | Adaptive Price Channel | Channel based on adaptive moving average with volatility bands. |
| [APZ](apz/Apz.md) | Adaptive Price Zone | Double-smoothed EMA volatility channel by Lee Leibfarth. Adapts to recent volatility. |
@@ -3,18 +3,18 @@ using Xunit;
namespace QuanTAlib.Tests;
public class AbberQuantowerTests
public class AberrQuantowerTests
{
[Fact]
public void Constructor_SetsDefaults()
{
var indicator = new AbberIndicator();
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("ABBER - Aberration Bands", indicator.Name);
Assert.Equal("ABERR - Aberration Bands", indicator.Name);
Assert.False(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
@@ -22,14 +22,14 @@ public class AbberQuantowerTests
[Fact]
public void MinHistoryDepths_MatchesPeriod()
{
var indicator = new AbberIndicator { Period = 25 };
var indicator = new AberrIndicator { Period = 25 };
Assert.Equal(25, indicator.MinHistoryDepths);
}
[Fact]
public void ShortName_IncludesParameters()
{
var indicator = new AbberIndicator { Period = 15, Multiplier = 1.5 };
var indicator = new AberrIndicator { Period = 15, Multiplier = 1.5 };
Assert.Contains("15", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("1.5", indicator.ShortName, StringComparison.Ordinal);
}
@@ -37,7 +37,7 @@ public class AbberQuantowerTests
[Fact]
public void Initialize_CreatesThreeLineSeries()
{
var indicator = new AbberIndicator { Period = 14 };
var indicator = new AberrIndicator { Period = 14 };
indicator.Initialize();
Assert.Equal(3, indicator.LinesSeries.Count);
@@ -49,7 +49,7 @@ public class AbberQuantowerTests
[Fact]
public void ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new AbberIndicator { Period = 3 };
var indicator = new AberrIndicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
@@ -67,7 +67,7 @@ public class AbberQuantowerTests
[Fact]
public void ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new AbberIndicator { Period = 3 };
var indicator = new AberrIndicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
@@ -83,7 +83,7 @@ public class AbberQuantowerTests
[Fact]
public void ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new AbberIndicator { Period = 5 };
var indicator = new AberrIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
@@ -98,7 +98,7 @@ public class AbberQuantowerTests
[Fact]
public void ProcessUpdate_EmptyData_HandlesGracefully()
{
var indicator = new AbberIndicator { Period = 5 };
var indicator = new AberrIndicator { Period = 5 };
indicator.Initialize();
var args = new UpdateArgs(UpdateReason.NewBar);
@@ -110,7 +110,7 @@ public class AbberQuantowerTests
[Fact]
public void MultipleUpdates_ProducesCorrectSequence()
{
var indicator = new AbberIndicator { Period = 5 };
var indicator = new AberrIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
@@ -136,7 +136,7 @@ public class AbberQuantowerTests
[Fact]
public void BandRelationship_UpperAboveLowerBelowMiddle()
{
var indicator = new AbberIndicator { Period = 5, Multiplier = 2.0 };
var indicator = new AberrIndicator { Period = 5, Multiplier = 2.0 };
indicator.Initialize();
var now = DateTime.UtcNow;
@@ -166,11 +166,11 @@ public class AbberQuantowerTests
var prices = new[] { 100, 105, 98, 110, 95, 115, 92, 118, 90, 120 };
// Narrow bands with multiplier 1.0
var narrowIndicator = new AbberIndicator { Period = 5, Multiplier = 1.0 };
var narrowIndicator = new AberrIndicator { Period = 5, Multiplier = 1.0 };
narrowIndicator.Initialize();
// Wide bands with multiplier 3.0
var wideIndicator = new AbberIndicator { Period = 5, Multiplier = 3.0 };
var wideIndicator = new AberrIndicator { Period = 5, Multiplier = 3.0 };
wideIndicator.Initialize();
for (int i = 0; i < prices.Length; i++)
@@ -191,7 +191,7 @@ public class AbberQuantowerTests
[Fact]
public void SourceType_CanBeChanged()
{
var indicator = new AbberIndicator { Source = SourceType.Close };
var indicator = new AberrIndicator { Source = SourceType.Close };
Assert.Equal(SourceType.Close, indicator.Source);
indicator.Source = SourceType.HLC3;
@@ -201,7 +201,7 @@ public class AbberQuantowerTests
[Fact]
public void ShowColdValues_CanBeToggled()
{
var indicator = new AbberIndicator { ShowColdValues = true };
var indicator = new AberrIndicator { ShowColdValues = true };
Assert.True(indicator.ShowColdValues);
indicator.ShowColdValues = false;
@@ -211,8 +211,8 @@ public class AbberQuantowerTests
[Fact]
public void SourceCodeLink_IsValid()
{
var indicator = new AbberIndicator();
var indicator = new AberrIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Abber.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("Aberr.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
}
@@ -5,12 +5,12 @@ using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
/// <summary>
/// ABBER (Aberration Bands) - Volatility bands using absolute deviation
/// 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 AbberIndicator : Indicator, IWatchlistIndicator
public sealed class AberrIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
public int Period { get; set; } = 20;
@@ -24,7 +24,7 @@ public sealed class AbberIndicator : Indicator, IWatchlistIndicator
"Low", SourceType.Low,
"Close", SourceType.Close,
"HL/2 (Median)", SourceType.HL2,
"OC/2 (Midpoint)", SourceType.OC2,
"Midbody (O+C)/2", SourceType.Midbody,
"OHL/3 (Mean)", SourceType.OHL3,
"HLC/3 (Typical)", SourceType.HLC3,
"OHLC/4 (Average)", SourceType.OHLC4,
@@ -35,7 +35,7 @@ public sealed class AbberIndicator : Indicator, IWatchlistIndicator
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Abber? _abber;
private Aberr? _aberr;
private Func<IHistoryItem, double>? _selector;
private readonly LineSeries _middleSeries;
private readonly LineSeries _upperSeries;
@@ -43,14 +43,14 @@ public sealed class AbberIndicator : Indicator, IWatchlistIndicator
public int MinHistoryDepths => Period;
public override string ShortName => $"ABBER {Period},{Multiplier:F1}";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/channels/abber/Abber.Quantower.cs";
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 AbberIndicator()
public AberrIndicator()
{
OnBackGround = true;
SeparateWindow = false;
Name = "ABBER - Aberration Bands";
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);
@@ -65,7 +65,7 @@ public sealed class AbberIndicator : Indicator, IWatchlistIndicator
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnInit()
{
_abber = new Abber(Period, Multiplier);
_aberr = new Aberr(Period, Multiplier);
_selector = Source.GetPriceSelector();
base.OnInit();
}
@@ -73,7 +73,7 @@ public sealed class AbberIndicator : Indicator, IWatchlistIndicator
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
if (HistoricalData.Count == 0 || _abber is null || _selector is null)
if (HistoricalData.Count == 0 || _aberr is null || _selector is null)
{
return;
}
@@ -82,10 +82,11 @@ public sealed class AbberIndicator : Indicator, IWatchlistIndicator
double value = _selector(item);
TValue input = new(item.TimeLeft, value);
_abber.Update(input, args.IsNewBar());
_aberr.Update(input, args.IsNewBar());
_middleSeries.SetValue(_abber.Last.Value, _abber.IsHot, ShowColdValues);
_upperSeries.SetValue(_abber.Upper.Value, _abber.IsHot, ShowColdValues);
_lowerSeries.SetValue(_abber.Lower.Value, _abber.IsHot, ShowColdValues);
_middleSeries.SetValue(_aberr.Last.Value, _aberr.IsHot, ShowColdValues);
_upperSeries.SetValue(_aberr.Upper.Value, _aberr.IsHot, ShowColdValues);
_lowerSeries.SetValue(_aberr.Lower.Value, _aberr.IsHot, ShowColdValues);
}
}
@@ -1,196 +1,196 @@
namespace QuanTAlib.Tests;
public class AbberTests
public class AberrTests
{
[Fact]
public void Abber_Constructor_ValidatesInput()
public void Aberr_Constructor_ValidatesInput()
{
// Period validation
Assert.Throws<ArgumentException>(() => new Abber(0));
Assert.Throws<ArgumentException>(() => new Abber(-1));
Assert.Throws<ArgumentException>(() => new Aberr(0));
Assert.Throws<ArgumentException>(() => new Aberr(-1));
// Multiplier validation
Assert.Throws<ArgumentException>(() => new Abber(10, 0));
Assert.Throws<ArgumentException>(() => new Abber(10, -1));
Assert.Throws<ArgumentException>(() => new Aberr(10, 0));
Assert.Throws<ArgumentException>(() => new Aberr(10, -1));
// Valid construction
var abber = new Abber(10);
Assert.NotNull(abber);
var aberr = new Aberr(10);
Assert.NotNull(aberr);
var abber2 = new Abber(20, 3.0);
Assert.NotNull(abber2);
var aberr2 = new Aberr(20, 3.0);
Assert.NotNull(aberr2);
}
[Fact]
public void Abber_Calc_ReturnsValue()
public void Aberr_Calc_ReturnsValue()
{
var abber = new Abber(10);
var aberr = new Aberr(10);
Assert.Equal(0, abber.Last.Value);
Assert.Equal(0, abber.Upper.Value);
Assert.Equal(0, abber.Lower.Value);
Assert.Equal(0, aberr.Last.Value);
Assert.Equal(0, aberr.Upper.Value);
Assert.Equal(0, aberr.Lower.Value);
TValue result = abber.Update(new TValue(DateTime.UtcNow, 100));
TValue result = aberr.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));
Assert.Equal(result.Value, aberr.Last.Value);
Assert.True(double.IsFinite(aberr.Upper.Value));
Assert.True(double.IsFinite(aberr.Lower.Value));
}
[Fact]
public void Abber_FirstValue_ReturnsExpected()
public void Aberr_FirstValue_ReturnsExpected()
{
var abber = new Abber(10);
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
abber.Update(new TValue(DateTime.UtcNow, 100));
aberr.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);
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 Abber_Calc_IsNew_AcceptsParameter()
public void Aberr_Calc_IsNew_AcceptsParameter()
{
var abber = new Abber(10);
var aberr = new Aberr(10);
abber.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
double value1 = abber.Last.Value;
aberr.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
double value1 = aberr.Last.Value;
abber.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
double value2 = abber.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 Abber_Calc_IsNew_False_UpdatesValue()
public void Aberr_Calc_IsNew_False_UpdatesValue()
{
var abber = new Abber(10);
var aberr = new Aberr(10);
abber.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
abber.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
double beforeUpdate = abber.Last.Value;
aberr.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
aberr.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
double beforeUpdate = aberr.Last.Value;
abber.Update(new TValue(DateTime.UtcNow, 120), isNew: false);
double afterUpdate = abber.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 Abber_Reset_ClearsState()
public void Aberr_Reset_ClearsState()
{
var abber = new Abber(10);
var aberr = new Aberr(10);
abber.Update(new TValue(DateTime.UtcNow, 100));
abber.Update(new TValue(DateTime.UtcNow, 105));
double middleBefore = abber.Last.Value;
aberr.Update(new TValue(DateTime.UtcNow, 100));
aberr.Update(new TValue(DateTime.UtcNow, 105));
double middleBefore = aberr.Last.Value;
abber.Reset();
aberr.Reset();
Assert.Equal(0, abber.Last.Value);
Assert.Equal(0, abber.Upper.Value);
Assert.Equal(0, abber.Lower.Value);
Assert.False(abber.IsHot);
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
abber.Update(new TValue(DateTime.UtcNow, 50));
Assert.NotEqual(0, abber.Last.Value);
Assert.NotEqual(middleBefore, abber.Last.Value);
aberr.Update(new TValue(DateTime.UtcNow, 50));
Assert.NotEqual(0, aberr.Last.Value);
Assert.NotEqual(middleBefore, aberr.Last.Value);
}
[Fact]
public void Abber_Properties_Accessible()
public void Aberr_Properties_Accessible()
{
var abber = new Abber(10, 2.5);
var aberr = new Aberr(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);
Assert.Equal(0, aberr.Last.Value);
Assert.False(aberr.IsHot);
Assert.Contains("Aberr", aberr.Name, StringComparison.Ordinal);
Assert.Equal(10, aberr.WarmupPeriod);
abber.Update(new TValue(DateTime.UtcNow, 100));
aberr.Update(new TValue(DateTime.UtcNow, 100));
Assert.NotEqual(0, abber.Last.Value);
Assert.NotEqual(0, aberr.Last.Value);
}
[Fact]
public void Abber_IsHot_BecomesTrueWhenBufferFull()
public void Aberr_IsHot_BecomesTrueWhenBufferFull()
{
var abber = new Abber(5);
var aberr = new Aberr(5);
Assert.False(abber.IsHot);
Assert.False(aberr.IsHot);
for (int i = 1; i <= 4; i++)
{
abber.Update(new TValue(DateTime.UtcNow, 100 + i));
Assert.False(abber.IsHot);
aberr.Update(new TValue(DateTime.UtcNow, 100 + i));
Assert.False(aberr.IsHot);
}
abber.Update(new TValue(DateTime.UtcNow, 105));
Assert.True(abber.IsHot);
aberr.Update(new TValue(DateTime.UtcNow, 105));
Assert.True(aberr.IsHot);
}
[Fact]
public void Abber_CalculatesCorrectBands()
public void Aberr_CalculatesCorrectBands()
{
var abber = new Abber(3, 2.0);
var aberr = new Aberr(3, 2.0);
// Bar 1: source = 100
// SMA = 100, Deviation = |100-100| = 0, AvgDev = 0
abber.Update(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100.0, abber.Last.Value, 1e-10);
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
abber.Update(new TValue(DateTime.UtcNow, 110));
Assert.Equal(105.0, abber.Last.Value, 1e-10);
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
abber.Update(new TValue(DateTime.UtcNow, 120));
Assert.Equal(110.0, abber.Last.Value, 1e-10);
Assert.Equal(120.0, abber.Upper.Value, 1e-10);
Assert.Equal(100.0, abber.Lower.Value, 1e-10);
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 Abber_SlidingWindow_Works()
public void Aberr_SlidingWindow_Works()
{
var abber = new Abber(3, 2.0);
var aberr = new Aberr(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));
aberr.Update(new TValue(DateTime.UtcNow, 100));
aberr.Update(new TValue(DateTime.UtcNow, 110));
aberr.Update(new TValue(DateTime.UtcNow, 120));
double middle1 = abber.Last.Value;
double middle1 = aberr.Last.Value;
// Add another value - window slides
abber.Update(new TValue(DateTime.UtcNow, 130));
aberr.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);
Assert.NotEqual(middle1, aberr.Last.Value);
Assert.Equal(120.0, aberr.Last.Value, 1e-10);
}
[Fact]
public void Abber_IterativeCorrections_RestoreToOriginalState()
public void Aberr_IterativeCorrections_RestoreToOriginalState()
{
var abber = new Abber(5);
var aberr = new Aberr(5);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
// Feed 10 new values
@@ -199,34 +199,34 @@ public class AbberTests
{
var bar = gbm.Next(isNew: true);
tenthInput = new TValue(bar.Time, bar.Close);
abber.Update(tenthInput, isNew: true);
aberr.Update(tenthInput, isNew: true);
}
// Remember state after 10 values
double middleAfterTen = abber.Last.Value;
double upperAfterTen = abber.Upper.Value;
double lowerAfterTen = abber.Lower.Value;
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);
abber.Update(new TValue(bar.Time, bar.Close), isNew: false);
aberr.Update(new TValue(bar.Time, bar.Close), isNew: false);
}
// Feed the remembered 10th input again with isNew=false
abber.Update(tenthInput, isNew: false);
aberr.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);
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 Abber_BatchCalc_MatchesIterativeCalc()
public void Aberr_BatchCalc_MatchesIterativeCalc()
{
var abberIterative = new Abber(10);
var aberrIterative = new Aberr(10);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
// Generate data
@@ -245,15 +245,15 @@ public class AbberTests
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);
aberrIterative.Update(item);
iterativeMiddle.Add(aberrIterative.Last.Value);
iterativeUpper.Add(aberrIterative.Upper.Value);
iterativeLower.Add(aberrIterative.Lower.Value);
}
// Calculate batch
var abberBatch = new Abber(10);
var (batchMiddle, batchUpper, batchLower) = abberBatch.Update(series);
var aberrBatch = new Aberr(10);
var (batchMiddle, batchUpper, batchLower) = aberrBatch.Update(series);
// Compare
Assert.Equal(iterativeMiddle.Count, batchMiddle.Count);
@@ -266,59 +266,59 @@ public class AbberTests
}
[Fact]
public void Abber_NaN_Input_UsesLastValidValue()
public void Aberr_NaN_Input_UsesLastValidValue()
{
var abber = new Abber(5);
var aberr = new Aberr(5);
// Feed some valid values
abber.Update(new TValue(DateTime.UtcNow, 100));
abber.Update(new TValue(DateTime.UtcNow, 105));
aberr.Update(new TValue(DateTime.UtcNow, 100));
aberr.Update(new TValue(DateTime.UtcNow, 105));
// Feed NaN - should use last valid value
var resultAfterNaN = abber.Update(new TValue(DateTime.UtcNow, double.NaN));
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(abber.Upper.Value));
Assert.True(double.IsFinite(abber.Lower.Value));
Assert.True(double.IsFinite(aberr.Upper.Value));
Assert.True(double.IsFinite(aberr.Lower.Value));
}
[Fact]
public void Abber_Infinity_Input_UsesLastValidValue()
public void Aberr_Infinity_Input_UsesLastValidValue()
{
var abber = new Abber(5);
var aberr = new Aberr(5);
// Feed some valid values
abber.Update(new TValue(DateTime.UtcNow, 100));
abber.Update(new TValue(DateTime.UtcNow, 105));
aberr.Update(new TValue(DateTime.UtcNow, 100));
aberr.Update(new TValue(DateTime.UtcNow, 105));
// Feed positive infinity
var resultAfterPosInf = abber.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
var resultAfterPosInf = aberr.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));
Assert.True(double.IsFinite(aberr.Upper.Value));
Assert.True(double.IsFinite(aberr.Lower.Value));
// Feed negative infinity
var resultAfterNegInf = abber.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
var resultAfterNegInf = aberr.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));
Assert.True(double.IsFinite(aberr.Upper.Value));
Assert.True(double.IsFinite(aberr.Lower.Value));
}
[Fact]
public void Abber_MultipleNaN_ContinuesWithLastValid()
public void Aberr_MultipleNaN_ContinuesWithLastValid()
{
var abber = new Abber(5);
var aberr = new Aberr(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));
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 = 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));
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));
@@ -327,7 +327,7 @@ public class AbberTests
}
[Fact]
public void Abber_StaticBatch_Works()
public void Aberr_StaticBatch_Works()
{
var series = new TSeries();
series.Add(DateTime.UtcNow, 100);
@@ -336,7 +336,7 @@ public class AbberTests
series.Add(DateTime.UtcNow, 130);
series.Add(DateTime.UtcNow, 140);
var (middle, upper, lower) = Abber.Batch(series, 3);
var (middle, upper, lower) = Aberr.Batch(series, 3);
Assert.Equal(5, middle.Count);
Assert.Equal(5, upper.Count);
@@ -352,29 +352,29 @@ public class AbberTests
}
[Fact]
public void Abber_Period1_ReturnsDirectCalculation()
public void Aberr_Period1_ReturnsDirectCalculation()
{
var abber = new Abber(1);
var aberr = new Aberr(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);
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
abber.Update(new TValue(DateTime.UtcNow, 110));
Assert.Equal(110.0, abber.Last.Value, 1e-10);
aberr.Update(new TValue(DateTime.UtcNow, 110));
Assert.Equal(110.0, aberr.Last.Value, 1e-10);
}
// ============== Span API Tests ==============
[Fact]
public void Abber_SpanBatch_ValidatesInput()
public void Aberr_SpanBatch_ValidatesInput()
{
double[] source = [100, 110, 120];
double[] middle = new double[3];
@@ -383,24 +383,24 @@ public class AbberTests
// Period must be > 0
Assert.Throws<ArgumentException>(() =>
Abber.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 0));
Aberr.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 0));
Assert.Throws<ArgumentException>(() =>
Abber.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), -1));
Aberr.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));
Aberr.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));
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>(() =>
Abber.Batch(source.AsSpan(), shortOutput.AsSpan(), upper.AsSpan(), lower.AsSpan(), 3));
Aberr.Batch(source.AsSpan(), shortOutput.AsSpan(), upper.AsSpan(), lower.AsSpan(), 3));
}
[Fact]
public void Abber_SpanBatch_MatchesTSeriesBatch()
public void Aberr_SpanBatch_MatchesTSeriesBatch()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
var series = new TSeries();
@@ -415,13 +415,13 @@ public class AbberTests
}
// Calculate with TSeries API
var (tseriesMiddle, tseriesUpper, tseriesLower) = Abber.Batch(series, 10);
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];
Abber.Batch(source.AsSpan(), spanMiddle.AsSpan(), spanUpper.AsSpan(), spanLower.AsSpan(), 10);
Aberr.Batch(source.AsSpan(), spanMiddle.AsSpan(), spanUpper.AsSpan(), spanLower.AsSpan(), 10);
// Compare results
for (int i = 0; i < 100; i++)
@@ -433,7 +433,7 @@ public class AbberTests
}
[Fact]
public void Abber_SpanBatch_ZeroAllocation()
public void Aberr_SpanBatch_ZeroAllocation()
{
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
double[] source = new double[10000];
@@ -447,7 +447,7 @@ public class AbberTests
}
// Warm up
Abber.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 100);
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]));
@@ -456,14 +456,14 @@ public class AbberTests
}
[Fact]
public void Abber_SpanBatch_HandlesNaN()
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];
Abber.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 3);
Aberr.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 3);
// All outputs should be finite
for (int i = 0; i < 5; i++)
@@ -475,7 +475,7 @@ public class AbberTests
}
[Fact]
public void Abber_AllModes_ProduceSameResult()
public void Aberr_AllModes_ProduceSameResult()
{
// Arrange
const int period = 10;
@@ -485,7 +485,7 @@ public class AbberTests
var series = bars.Close;
// 1. Batch Mode
var (batchMiddle, batchUpper, batchLower) = Abber.Batch(series, period, multiplier);
var (batchMiddle, batchUpper, batchLower) = Aberr.Batch(series, period, multiplier);
double expectedMiddle = batchMiddle.Last.Value;
double expectedUpper = batchUpper.Last.Value;
double expectedLower = batchLower.Last.Value;
@@ -495,10 +495,10 @@ public class AbberTests
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);
Aberr.Batch(source.AsSpan(), spanMiddle.AsSpan(), spanUpper.AsSpan(), spanLower.AsSpan(), period, multiplier);
// 3. Streaming Mode
var streamingInd = new Abber(period, multiplier);
var streamingInd = new Aberr(period, multiplier);
foreach (var item in series)
{
streamingInd.Update(item);
@@ -509,7 +509,7 @@ public class AbberTests
// 4. Eventing Mode
var pubSource = new TSeries();
var eventingInd = new Abber(pubSource, period, multiplier);
var eventingInd = new Aberr(pubSource, period, multiplier);
foreach (var item in series)
{
pubSource.Add(item);
@@ -533,26 +533,26 @@ public class AbberTests
}
[Fact]
public void Abber_Chainability_Works()
public void Aberr_Chainability_Works()
{
var source = new TSeries();
var abber = new Abber(source, 10);
var aberr = new Aberr(source, 10);
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, abber.Last.Value);
Assert.Equal(100, aberr.Last.Value);
}
[Fact]
public void Abber_WarmupPeriod_IsSetCorrectly()
public void Aberr_WarmupPeriod_IsSetCorrectly()
{
var abber = new Abber(10);
Assert.Equal(10, abber.WarmupPeriod);
var aberr = new Aberr(10);
Assert.Equal(10, aberr.WarmupPeriod);
}
[Fact]
public void Abber_Prime_SetsStateCorrectly()
public void Aberr_Prime_SetsStateCorrectly()
{
var abber = new Abber(3, 2.0);
var aberr = new Aberr(3, 2.0);
var series = new TSeries();
// Add 5 values
@@ -562,21 +562,21 @@ public class AbberTests
series.Add(DateTime.UtcNow, 130);
series.Add(DateTime.UtcNow, 140);
abber.Prime(series);
aberr.Prime(series);
Assert.True(abber.IsHot);
Assert.True(aberr.IsHot);
// Last 3 values: 120, 130, 140 -> SMA = 130
Assert.Equal(130.0, abber.Last.Value, 1e-10);
Assert.Equal(130.0, aberr.Last.Value, 1e-10);
// Verify it continues correctly
abber.Update(new TValue(DateTime.UtcNow, 150));
aberr.Update(new TValue(DateTime.UtcNow, 150));
// New window: 130, 140, 150 -> SMA = 140
Assert.Equal(140.0, abber.Last.Value, 1e-10);
Assert.Equal(140.0, aberr.Last.Value, 1e-10);
}
[Fact]
public void Abber_Calculate_ReturnsCorrectResultsAndHotIndicator()
public void Aberr_Calculate_ReturnsCorrectResultsAndHotIndicator()
{
var series = new TSeries();
series.Add(DateTime.UtcNow, 100);
@@ -585,7 +585,7 @@ public class AbberTests
series.Add(DateTime.UtcNow, 130);
series.Add(DateTime.UtcNow, 140);
var ((middle, upper, lower), indicator) = Abber.Calculate(series, 3, 2.0);
var ((middle, upper, lower), indicator) = Aberr.Calculate(series, 3, 2.0);
// Check results
Assert.Equal(5, middle.Count);
@@ -603,7 +603,7 @@ public class AbberTests
}
[Fact]
public void Abber_DifferentMultipliers_Work()
public void Aberr_DifferentMultipliers_Work()
{
var series = new TSeries();
for (int i = 0; i < 10; i++)
@@ -612,10 +612,10 @@ public class AbberTests
}
// Multiplier 1.0
var (middle1, upper1, _) = Abber.Batch(series, 5, 1.0);
var (middle1, upper1, _) = Aberr.Batch(series, 5, 1.0);
// Multiplier 3.0
var (middle3, upper3, _) = Abber.Batch(series, 5, 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);
@@ -627,47 +627,47 @@ public class AbberTests
}
[Fact]
public void Abber_FlatLine_ReturnsSameValues()
public void Aberr_FlatLine_ReturnsSameValues()
{
var abber = new Abber(10);
var aberr = new Aberr(10);
for (int i = 0; i < 20; i++)
{
abber.Update(new TValue(DateTime.UtcNow, 100));
aberr.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);
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 Abber_Pub_EventFires()
public void Aberr_Pub_EventFires()
{
var abber = new Abber(10);
var aberr = new Aberr(10);
bool eventFired = false;
abber.Pub += (object? _, in TValueEventArgs _) => eventFired = true;
aberr.Pub += (object? _, in TValueEventArgs _) => eventFired = true;
abber.Update(new TValue(DateTime.UtcNow, 100));
aberr.Update(new TValue(DateTime.UtcNow, 100));
Assert.True(eventFired);
}
[Fact]
public void Abber_BandsAreSymmetric()
public void Aberr_BandsAreSymmetric()
{
var abber = new Abber(10, 2.0);
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);
abber.Update(new TValue(bar.Time, bar.Close));
aberr.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;
double upperDiff = aberr.Upper.Value - aberr.Last.Value;
double lowerDiff = aberr.Last.Value - aberr.Lower.Value;
Assert.Equal(upperDiff, lowerDiff, 1e-10);
}
@@ -3,12 +3,12 @@ using Xunit.Abstractions;
namespace QuanTAlib.Tests;
/// <summary>
/// Validation tests for Abber indicator.
/// Validation tests for Aberr indicator.
/// Note: Skender.Stock.Indicators, TA-Lib, Tulip, and OoplesFinance do not provide
/// Abber (Aberration Bands) implementation for cross-validation. These tests validate
/// Aberr (Aberration Bands) implementation for cross-validation. These tests validate
/// against manual calculations and internal consistency across all API modes.
/// </summary>
public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
public sealed class AberrValidationTests(ITestOutputHelper output) : IDisposable
{
private readonly ValidationTestData _testData = new();
private bool _disposed;
@@ -48,8 +48,8 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
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);
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);
@@ -61,7 +61,7 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
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");
output.WriteLine("Aberr manual calculation (period 3) validated successfully");
}
[Fact]
@@ -78,13 +78,13 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
series.Add(new TValue(time.AddMinutes(i), values[i]));
}
var abber = new Abber(5, 2.0);
var (middle, _, _) = abber.Update(series);
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("Abber manual calculation (period 5) validated successfully");
output.WriteLine("Aberr manual calculation (period 5) validated successfully");
}
[Fact]
@@ -101,9 +101,9 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
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);
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);
@@ -117,7 +117,7 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
Assert.Equal(bw1 * 2.0, bw2, 1e-10);
Assert.Equal(bw1 * 3.0, bw3, 1e-10);
output.WriteLine("Abber multiplier effect validated successfully");
output.WriteLine("Aberr multiplier effect validated successfully");
}
[Fact]
@@ -128,18 +128,18 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
foreach (var period in periods)
{
// Batch mode using instance
var abber = new Abber(period, 2.0);
var (qMiddle, qUpper, qLower) = abber.Update(_testData.Data);
var aberr = new Aberr(period, 2.0);
var (qMiddle, qUpper, qLower) = aberr.Update(_testData.Data);
// Static batch
var (sMiddle, sUpper, sLower) = Abber.Batch(_testData.Data, period, 2.0);
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("Abber Batch modes consistency validated successfully");
output.WriteLine("Aberr Batch modes consistency validated successfully");
}
[Fact]
@@ -150,27 +150,27 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
foreach (var period in periods)
{
// Streaming mode
var streamingAbber = new Abber(period, 2.0);
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)
{
streamingAbber.Update(item);
streamMiddle.Add(streamingAbber.Last);
streamUpper.Add(streamingAbber.Upper);
streamLower.Add(streamingAbber.Lower);
streamingAberr.Update(item);
streamMiddle.Add(streamingAberr.Last);
streamUpper.Add(streamingAberr.Upper);
streamLower.Add(streamingAberr.Lower);
}
// Batch mode for comparison
var (batchMiddle, batchUpper, batchLower) = Abber.Batch(_testData.Data, period, 2.0);
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("Abber Streaming mode consistency validated successfully");
output.WriteLine("Aberr Streaming mode consistency validated successfully");
}
[Fact]
@@ -188,11 +188,11 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
double[] spanUpper = new double[len];
double[] spanLower = new double[len];
Abber.Batch(source.AsSpan(), spanMiddle.AsSpan(), spanUpper.AsSpan(), spanLower.AsSpan(),
Aberr.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);
var (batchMiddle, batchUpper, batchLower) = Aberr.Batch(_testData.Data, period, 2.0);
// Verify match
for (int i = 0; i < len; i++)
@@ -202,7 +202,7 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
Assert.Equal(batchLower[i].Value, spanLower[i], 9);
}
}
output.WriteLine("Abber Span mode consistency validated successfully");
output.WriteLine("Aberr Span mode consistency validated successfully");
}
[Fact]
@@ -214,7 +214,7 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
{
// Eventing mode
var pubSource = new TSeries();
var eventingInd = new Abber(pubSource, period, 2.0);
var eventingInd = new Aberr(pubSource, period, 2.0);
var eventMiddle = new TSeries();
var eventUpper = new TSeries();
var eventLower = new TSeries();
@@ -228,14 +228,14 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
}
// Batch mode for comparison
var (batchMiddle, batchUpper, batchLower) = Abber.Batch(_testData.Data, period, 2.0);
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("Abber Eventing mode consistency validated successfully");
output.WriteLine("Aberr Eventing mode consistency validated successfully");
}
[Fact]
@@ -245,7 +245,7 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
foreach (var period in periods)
{
var ((_, _, _), indicator) = Abber.Calculate(_testData.Data, period, 2.0);
var ((_, _, _), indicator) = Aberr.Calculate(_testData.Data, period, 2.0);
// Verify indicator is hot
Assert.True(indicator.IsHot);
@@ -264,14 +264,14 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
indicator.Update(nextValue);
Assert.True(indicator.IsHot);
}
output.WriteLine("Abber Calculate method validated successfully");
output.WriteLine("Aberr 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);
var (middle, upper, lower) = Aberr.Batch(_testData.Data, 100, 2.0);
// All outputs should be finite
ValidationHelper.VerifyAllFinite(middle, startIndex: 0);
@@ -287,7 +287,7 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
$"Middle ({middle[i].Value}) should be >= Lower ({lower[i].Value}) at index {i}");
}
output.WriteLine("Abber large dataset (5000 bars) validated successfully");
output.WriteLine("Aberr large dataset (5000 bars) validated successfully");
}
[Fact]
@@ -296,7 +296,7 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
// 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);
var (middle, upper, lower) = Aberr.Batch(_testData.Data, 20, 2.0);
// After warmup, verify symmetry
for (int i = 20; i < _testData.Data.Count; i++)
@@ -307,7 +307,7 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
Assert.Equal(upperDiff, lowerDiff, 1e-9);
}
output.WriteLine("Abber band width symmetry validated successfully");
output.WriteLine("Aberr band width symmetry validated successfully");
}
[Fact]
@@ -317,10 +317,10 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
int period = 20;
// Full batch calculation
var (batchMiddle, batchUpper, batchLower) = Abber.Batch(_testData.Data, period, 2.0);
var (batchMiddle, batchUpper, batchLower) = Aberr.Batch(_testData.Data, period, 2.0);
// Prime indicator with subset and continue
var primedIndicator = new Abber(period, 2.0);
var primedIndicator = new Aberr(period, 2.0);
var subset = new TSeries();
for (int i = 0; i < 100; i++)
{
@@ -339,7 +339,7 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
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");
output.WriteLine("Aberr Prime method validated successfully");
}
[Fact]
@@ -348,19 +348,19 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
// Verify the middle band is exactly the SMA
int period = 20;
var abber = new Abber(period, 2.0);
var aberr = new Aberr(period, 2.0);
var sma = new Sma(period);
var abberResults = abber.Update(_testData.Data);
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, abberResults.Middle[i].Value, 1e-10);
Assert.Equal(smaResults[i].Value, aberrResults.Middle[i].Value, 1e-10);
}
output.WriteLine("Abber middle band matches SMA validated successfully");
output.WriteLine("Aberr middle band matches SMA validated successfully");
}
[Fact]
@@ -378,8 +378,8 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
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);
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);
@@ -390,7 +390,7 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
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");
output.WriteLine("Aberr deviation calculation validated successfully");
}
[Fact]
@@ -401,7 +401,7 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
foreach (var period in periods)
{
var (middle, upper, lower) = Abber.Batch(_testData.Data, period, 2.0);
var (middle, upper, lower) = Aberr.Batch(_testData.Data, period, 2.0);
// All values should be finite
for (int i = 0; i < middle.Count; i++)
@@ -419,6 +419,6 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
}
}
output.WriteLine($"Abber consistency across {periods.Length} periods validated successfully");
output.WriteLine($"Aberr consistency across {periods.Length} periods validated successfully");
}
}
@@ -6,7 +6,7 @@ using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// Abber: Aberration Bands
/// Aberr: Aberration Bands
/// </summary>
/// <remarks>
/// Aberration Bands measure price deviation from a central moving average using absolute
@@ -27,10 +27,10 @@ namespace QuanTAlib;
/// - Bands expand during volatile periods and contract during consolidation
///
/// Sources:
/// Pine Script implementation: https://github.com/mihakralj/pinescript/blob/main/indicators/channels/abber.pine
/// Pine Script implementation: https://github.com/mihakralj/pinescript/blob/main/indicators/channels/aberr.pine
/// </remarks>
[SkipLocalsInit]
public sealed class Abber : ITValuePublisher, IDisposable
public sealed class Aberr : ITValuePublisher, IDisposable
{
private readonly int _period;
private readonly double _multiplier;
@@ -88,11 +88,11 @@ public sealed class Abber : ITValuePublisher, IDisposable
public event TValuePublishedHandler? Pub;
/// <summary>
/// Creates Abber with specified period and multiplier.
/// 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 Abber(int period, double multiplier = 2.0)
public Aberr(int period, double multiplier = 2.0)
{
if (period <= 0)
{
@@ -108,15 +108,15 @@ public sealed class Abber : ITValuePublisher, IDisposable
_multiplier = multiplier;
_sourceBuffer = new RingBuffer(period);
_deviationBuffer = new RingBuffer(period);
Name = $"Abber({period},{multiplier:F2})";
Name = $"Aberr({period},{multiplier:F2})";
WarmupPeriod = period;
_handler = HandleValue;
}
/// <summary>
/// Creates Abber with TSeries source.
/// Creates Aberr with TSeries source.
/// </summary>
public Abber(TSeries source, int period, double multiplier = 2.0) : this(period, multiplier)
public Aberr(TSeries source, int period, double multiplier = 2.0) : this(period, multiplier)
{
_source = source ?? throw new ArgumentNullException(nameof(source));
Prime(source);
@@ -124,9 +124,9 @@ public sealed class Abber : ITValuePublisher, IDisposable
}
/// <summary>
/// Creates Abber with ITValuePublisher source.
/// Creates Aberr with ITValuePublisher source.
/// </summary>
public Abber(ITValuePublisher source, int period, double multiplier = 2.0) : this(period, multiplier)
public Aberr(ITValuePublisher source, int period, double multiplier = 2.0) : this(period, multiplier)
{
_source = source ?? throw new ArgumentNullException(nameof(source));
_source.Pub += _handler;
@@ -408,7 +408,7 @@ public sealed class Abber : ITValuePublisher, IDisposable
/////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary>
/// Output buffers for batch Abber calculation.
/// Output buffers for batch Aberr calculation.
/// </summary>
[StructLayout(LayoutKind.Auto)]
#pragma warning disable S1104 // Fields should not have public accessibility
@@ -457,16 +457,16 @@ public sealed class Abber : ITValuePublisher, IDisposable
}
/// <summary>
/// Calculates Abber for the entire TSeries using a new instance.
/// 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 abber = new Abber(period, multiplier);
return abber.Update(source);
var aberr = new Aberr(period, multiplier);
return aberr.Update(source);
}
/// <summary>
/// Calculates Abber in-place using spans for maximum performance.
/// Calculates Aberr in-place using spans for maximum performance.
/// Zero-allocation method.
/// </summary>
/// <param name="source">Source price values</param>
@@ -484,7 +484,7 @@ public sealed class Abber : ITValuePublisher, IDisposable
}
/// <summary>
/// Calculates Abber in-place using spans for maximum performance.
/// Calculates Aberr in-place using spans for maximum performance.
/// Zero-allocation method.
/// </summary>
/// <param name="source">Source price values</param>
@@ -677,17 +677,17 @@ public sealed class Abber : ITValuePublisher, IDisposable
}
/// <summary>
/// Runs a high-performance batch calculation and returns a "Hot" Abber instance.
/// Runs a high-performance batch calculation and returns a "Hot" Aberr instance.
/// </summary>
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Abber Indicator) Calculate(TSeries source, int period, double multiplier = 2.0)
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Aberr Indicator) Calculate(TSeries source, int period, double multiplier = 2.0)
{
var abber = new Abber(period, multiplier);
var results = abber.Update(source);
return (results, abber);
var aberr = new Aberr(period, multiplier);
var results = aberr.Update(source);
return (results, aberr);
}
/// <summary>
/// Disposes the Abber instance, unsubscribing from the source publisher.
/// Disposes the Aberr instance, unsubscribing from the source publisher.
/// This method is idempotent.
/// </summary>
public void Dispose()
@@ -1,4 +1,4 @@
# ABBER: Aberration Bands
# ABERR: Aberration Bands
| Property | Value |
| ---------------- | -------------------------------- |
@@ -11,19 +11,19 @@
### TL;DR
- ABBER measures price deviation from a central moving average using mean absolute deviation rather than standard deviation, producing dynamic bands ...
- 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.
ABBER 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), ABBER 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, ABBER 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.
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 ABBER. 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 ABBER 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 ABBER's favor.
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
@@ -72,12 +72,12 @@ For a normal distribution:
$$\text{MAD} = \sigma \sqrt{\frac{2}{\pi}} \approx 0.7979\,\sigma$$
Therefore ABBER with $k = 2.0$ captures approximately the same range as Bollinger Bands with $k \approx 1.596$.
Therefore ABERR with $k = 2.0$ captures approximately the same range as Bollinger Bands with $k \approx 1.596$.
### Pseudo-code
```
function ABBER(source, ma_line, period, multiplier):
function ABERR(source, ma_line, period, multiplier):
// Deviation from center line
deviation = |source - ma_line|
@@ -103,7 +103,7 @@ function ABBER(source, ma_line, period, multiplier):
### Operation Count (Streaming Mode)
ABBER maintains two running-sum ring buffers (SMA of price and SMA of absolute deviations), each updated in $O(1)$:
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 |
| :--- | :---: | :---: | :---: |
@@ -1,7 +1,7 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Aberration (ABBER)", "ABBER", overlay=true)
indicator("Aberration (ABERR)", "ABERR", overlay=true)
//@function Calculates Aberration bands measuring deviation from a central moving average
//@param source Series to calculate aberration from
@@ -10,7 +10,7 @@ indicator("Aberration (ABBER)", "ABBER", overlay=true)
//@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) =>
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))
@@ -38,7 +38,7 @@ ma_line = switch i_ma_type
=> ta.sma(i_source, i_period)
// Calculation
[upper_band, lower_band, deviation] = abber(i_source, ma_line, i_period, i_multiplier)
[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)
+3 -3
View File
@@ -12,7 +12,7 @@ public class MaenvIndicatorTests
Assert.Equal(20, ind.Period);
Assert.Equal(1.0, ind.Percentage);
Assert.Equal(MaenvType.EMA, ind.maType);
Assert.Equal(MaenvType.EMA, ind.MaType);
Assert.Equal(PriceType.Close, ind.SourceType);
Assert.True(ind.ShowColdValues);
Assert.Equal("Maenv - Moving Average Envelope", ind.Name);
@@ -30,7 +30,7 @@ public class MaenvIndicatorTests
[Fact]
public void ShortName_ReflectsParameters()
{
var ind = new MaenvIndicator { Period = 12, Percentage = 2.5, maType = MaenvType.SMA };
var ind = new MaenvIndicator { Period = 12, Percentage = 2.5, MaType = MaenvType.SMA };
Assert.Contains("12", ind.ShortName, StringComparison.Ordinal);
Assert.Contains("2.5", ind.ShortName, StringComparison.Ordinal);
Assert.Contains("SMA", ind.ShortName, StringComparison.Ordinal);
@@ -213,7 +213,7 @@ public class MaenvIndicatorTests
{
foreach (MaenvType maType in Enum.GetValues<MaenvType>())
{
var ind = new MaenvIndicator { Period = 10, Percentage = 2.0, maType = maType };
var ind = new MaenvIndicator { Period = 10, Percentage = 2.0, MaType = maType };
ind.Initialize();
var now = DateTime.UtcNow;