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