Remove multiple Pine Script indicators: SSFDSP, STARCHANNEL, STBANDS, STC, UBANDS, UCHANNEL, VWAPBANDS, and VWAPSD. These indicators were deleted to streamline the library and remove unused or redundant code.

This commit is contained in:
Miha Kralj
2026-02-20 18:44:56 -08:00
parent 3dd05f23e4
commit cbeefc9d64
283 changed files with 23963 additions and 3838 deletions
@@ -11,7 +11,7 @@ public class AccBandsIndicatorTests
var indicator = new AccBandsIndicator();
Assert.Equal(20, indicator.Period);
Assert.Equal(2.0, indicator.Factor);
Assert.Equal(4.0, indicator.Factor);
Assert.True(indicator.ShowColdValues);
Assert.Equal("AccBands - Acceleration Bands", indicator.Name);
Assert.False(indicator.SeparateWindow);
@@ -123,7 +123,7 @@ public class AccBandsIndicatorTests
[Fact]
public void BandRelationship_UpperAboveLowerBelowMiddle()
{
var indicator = new AccBandsIndicator { Period = 5, Factor = 2.0 };
var indicator = new AccBandsIndicator { Period = 5, Factor = 4.0 };
indicator.Initialize();
var now = DateTime.UtcNow;
@@ -147,12 +147,12 @@ public class AccBandsIndicatorTests
{
var now = DateTime.UtcNow;
// Narrow bands with factor 1.0
var narrowIndicator = new AccBandsIndicator { Period = 5, Factor = 1.0 };
// Narrow bands with factor 2.0
var narrowIndicator = new AccBandsIndicator { Period = 5, Factor = 2.0 };
narrowIndicator.Initialize();
// Wide bands with factor 3.0
var wideIndicator = new AccBandsIndicator { Period = 5, Factor = 3.0 };
// Wide bands with factor 6.0
var wideIndicator = new AccBandsIndicator { Period = 5, Factor = 6.0 };
wideIndicator.Initialize();
for (int i = 0; i < 10; i++)
@@ -183,8 +183,8 @@ public class AccBandsIndicatorTests
[Fact]
public void Factor_CanBeChanged()
{
var indicator = new AccBandsIndicator { Factor = 2.0 };
Assert.Equal(2.0, indicator.Factor);
var indicator = new AccBandsIndicator { Factor = 4.0 };
Assert.Equal(4.0, indicator.Factor);
indicator.Factor = 3.5;
Assert.Equal(3.5, indicator.Factor);
+3 -3
View File
@@ -9,7 +9,7 @@ namespace QuanTAlib;
/// <summary>
/// AccBands: Acceleration Bands - Quantower Indicator Adapter
/// Volatility-based channel indicator developed by Price Headley that creates
/// an adaptive price envelope around a moving average.
/// an adaptive price envelope using per-bar normalized width adjustment.
/// </summary>
public sealed class AccBandsIndicator : Indicator, IWatchlistIndicator
{
@@ -17,7 +17,7 @@ public sealed class AccBandsIndicator : Indicator, IWatchlistIndicator
public int Period { get; set; } = 20;
[InputParameter("Factor", sortIndex: 11, minimum: 0.1, maximum: 10.0, increment: 0.1, decimalPlaces: 2)]
public double Factor { get; set; } = 2.0;
public double Factor { get; set; } = 4.0;
[InputParameter("Show Cold Values", sortIndex: 100)]
public bool ShowColdValues { get; set; } = true;
@@ -30,7 +30,7 @@ public sealed class AccBandsIndicator : Indicator, IWatchlistIndicator
public AccBandsIndicator()
{
Name = "AccBands - Acceleration Bands";
Description = "Volatility-based adaptive price channel using SMA of High, Low, and Close";
Description = "Volatility-based adaptive price channel using per-bar normalized width (Headley)";
SeparateWindow = false;
OnBackGround = true;
}
+94 -68
View File
@@ -45,15 +45,16 @@ public class AccBandsTests
var accBands = new AccBands(10);
// First bar: O=100, H=105, L=95, C=102
// SMA of one value: high=105, low=95, close=102
// BandWidth = (105 - 95) * 2.0 = 20
// Middle = 102, Upper = 105 + 20 = 125, Lower = 95 - 20 = 75
// w = (105-95)/(105+95) = 10/200 = 0.05
// adjHigh = 105 * (1 + 4*0.05) = 105 * 1.2 = 126
// adjLow = 95 * (1 - 4*0.05) = 95 * 0.8 = 76
// Middle = 102, Upper = 126, Lower = 76
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
accBands.Update(bar);
Assert.Equal(102.0, accBands.Last.Value, 1e-10);
Assert.Equal(125.0, accBands.Upper.Value, 1e-10);
Assert.Equal(75.0, accBands.Lower.Value, 1e-10);
Assert.Equal(126.0, accBands.Upper.Value, 1e-10);
Assert.Equal(76.0, accBands.Lower.Value, 1e-10);
}
[Fact]
@@ -156,31 +157,40 @@ public class AccBandsTests
[Fact]
public void AccBands_CalculatesCorrectBands()
{
var accBands = new AccBands(3, 2.0);
var accBands = new AccBands(3, 4.0);
// Bar 1: H=110, L=90, C=100
// w1 = (110-90)/(110+90) = 20/200 = 0.1
// adjH1 = 110*(1+4*0.1) = 110*1.4 = 154
// adjL1 = 90*(1-4*0.1) = 90*0.6 = 54
accBands.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000));
// Bar 2: H=115, L=95, C=105
// w2 = (115-95)/(115+95) = 20/210 ≈ 0.095238
// adjH2 = 115*(1+4*0.095238) = 115*1.380952 ≈ 158.80952
// adjL2 = 95*(1-4*0.095238) = 95*0.619048 ≈ 58.80952
accBands.Update(new TBar(DateTime.UtcNow, 105, 115, 95, 105, 1000));
// Bar 3: H=120, L=100, C=110
// w3 = (120-100)/(120+100) = 20/220 ≈ 0.090909
// adjH3 = 120*(1+4*0.090909) = 120*1.363636 ≈ 163.63636
// adjL3 = 100*(1-4*0.090909) = 100*0.636364 ≈ 63.63636
accBands.Update(new TBar(DateTime.UtcNow, 110, 120, 100, 110, 1000));
// SMA(3) of High: (110 + 115 + 120) / 3 = 115
// SMA(3) of Low: (90 + 95 + 100) / 3 = 95
// SMA(3) of Close: (100 + 105 + 110) / 3 = 105
// BandWidth = (115 - 95) * 2.0 = 40
// Upper = 115 + 40 = 155
// Lower = 95 - 40 = 55
// SMA(3) of adjHigh: (154 + 158.80952 + 163.63636) / 3 158.81529
// SMA(3) of adjLow: (54 + 58.80952 + 63.63636) / 3 ≈ 58.81529
// SMA(3) of Close: (100+105+110)/3 = 105
double expectedUpper = (154.0 + 115.0 * (1.0 + 4.0 * 20.0 / 210.0) + 120.0 * (1.0 + 4.0 * 20.0 / 220.0)) / 3.0;
double expectedLower = (54.0 + 95.0 * (1.0 - 4.0 * 20.0 / 210.0) + 100.0 * (1.0 - 4.0 * 20.0 / 220.0)) / 3.0;
Assert.Equal(105.0, accBands.Last.Value, 1e-10);
Assert.Equal(155.0, accBands.Upper.Value, 1e-10);
Assert.Equal(55.0, accBands.Lower.Value, 1e-10);
Assert.Equal(expectedUpper, accBands.Upper.Value, 1e-10);
Assert.Equal(expectedLower, accBands.Lower.Value, 1e-10);
}
[Fact]
public void AccBands_SlidingWindow_Works()
{
var accBands = new AccBands(3, 2.0);
var accBands = new AccBands(3, 4.0);
// Bar 1: H=110, L=90, C=100
accBands.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000));
@@ -191,20 +201,18 @@ public class AccBandsTests
double middle1 = accBands.Last.Value;
// Bar 4: H=125, L=105, C=115 - Window slides: [115, 120, 125], [95, 100, 105], [105, 110, 115]
// Bar 4: H=125, L=105, C=115 - Window slides to bars [2,3,4]
// w4 = (125-105)/(125+105) = 20/230 ≈ 0.086957
// adjH4 = 125*(1+4*0.086957) = 125*1.347826 ≈ 168.47826
// adjL4 = 105*(1-4*0.086957) = 105*0.652174 ≈ 68.47826
accBands.Update(new TBar(DateTime.UtcNow, 115, 125, 105, 115, 1000));
// SMA(3) of High: (115 + 120 + 125) / 3 = 120
// SMA(3) of Low: (95 + 100 + 105) / 3 = 100
// SMA(3) of Close: (105 + 110 + 115) / 3 = 110
// BandWidth = (120 - 100) * 2.0 = 40
// Upper = 120 + 40 = 160
// Lower = 100 - 40 = 60
Assert.NotEqual(middle1, accBands.Last.Value);
Assert.Equal(110.0, accBands.Last.Value, 1e-10);
Assert.Equal(160.0, accBands.Upper.Value, 1e-10);
Assert.Equal(60.0, accBands.Lower.Value, 1e-10);
// Verify Upper > Middle > Lower
Assert.True(accBands.Upper.Value > accBands.Last.Value);
Assert.True(accBands.Lower.Value < accBands.Last.Value);
}
[Fact]
@@ -377,17 +385,22 @@ public class AccBandsTests
var accBands = new AccBands(1);
// Single bar: H=110, L=90, C=100
// BandWidth = (110 - 90) * 2.0 = 40
// w = (110-90)/(110+90) = 20/200 = 0.1
// adjHigh = 110*(1+4*0.1) = 110*1.4 = 154
// adjLow = 90*(1-4*0.1) = 90*0.6 = 54
accBands.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000));
Assert.Equal(100.0, accBands.Last.Value, 1e-10);
Assert.Equal(150.0, accBands.Upper.Value, 1e-10); // 110 + 40
Assert.Equal(50.0, accBands.Lower.Value, 1e-10); // 90 - 40
Assert.Equal(154.0, accBands.Upper.Value, 1e-10);
Assert.Equal(54.0, accBands.Lower.Value, 1e-10);
// Next bar: H=120, L=100, C=110 (window is 1, so only this bar counts)
// w = (120-100)/(120+100) = 20/220 ≈ 0.090909
// adjHigh = 120*(1+4*0.090909) = 120*1.363636 ≈ 163.63636
// adjLow = 100*(1-4*0.090909) = 100*0.636364 ≈ 63.63636
accBands.Update(new TBar(DateTime.UtcNow, 110, 120, 100, 110, 1000));
Assert.Equal(110.0, accBands.Last.Value, 1e-10);
Assert.Equal(160.0, accBands.Upper.Value, 1e-10); // 120 + 40
Assert.Equal(60.0, accBands.Lower.Value, 1e-10); // 100 - 40
Assert.Equal(120.0 * (1.0 + 4.0 * 20.0 / 220.0), accBands.Upper.Value, 1e-10);
Assert.Equal(100.0 * (1.0 - 4.0 * 20.0 / 220.0), accBands.Lower.Value, 1e-10);
}
// ============== Span API Tests ==============
@@ -477,14 +490,22 @@ public class AccBandsTests
AccBands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(),
middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 3);
// After warmup (index 2):
// SMA(3) of High: (110+115+120)/3 = 115
// SMA(3) of Low: (90+95+100)/3 = 95
// After warmup (index 2): bars 0,1,2
// Bar 0: H=110, L=90 => w=20/200=0.1, adjH=110*1.4=154, adjL=90*0.6=54
// Bar 1: H=115, L=95 => w=20/210, adjH=115*(1+4*20/210), adjL=95*(1-4*20/210)
// Bar 2: H=120, L=100 => w=20/220, adjH=120*(1+4*20/220), adjL=100*(1-4*20/220)
// SMA(3) of Close: (100+105+110)/3 = 105
// BandWidth = (115-95) * 2.0 = 40
double adjH0 = 110.0 * (1.0 + 4.0 * 20.0 / 200.0);
double adjH1 = 115.0 * (1.0 + 4.0 * 20.0 / 210.0);
double adjH2 = 120.0 * (1.0 + 4.0 * 20.0 / 220.0);
double adjL0 = 90.0 * (1.0 - 4.0 * 20.0 / 200.0);
double adjL1 = 95.0 * (1.0 - 4.0 * 20.0 / 210.0);
double adjL2 = 100.0 * (1.0 - 4.0 * 20.0 / 220.0);
Assert.Equal(105.0, middle[2], 1e-10);
Assert.Equal(155.0, upper[2], 1e-10); // 115 + 40
Assert.Equal(55.0, lower[2], 1e-10); // 95 - 40
Assert.Equal((adjH0 + adjH1 + adjH2) / 3.0, upper[2], 1e-10);
Assert.Equal((adjL0 + adjL1 + adjL2) / 3.0, lower[2], 1e-10);
}
[Fact]
@@ -543,7 +564,7 @@ public class AccBandsTests
{
// Arrange
const int period = 10;
double factor = 2.0;
double factor = 4.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));
@@ -618,7 +639,7 @@ public class AccBandsTests
[Fact]
public void AccBands_Prime_SetsStateCorrectly()
{
var accBands = new AccBands(3, 2.0);
var accBands = new AccBands(3, 4.0);
var series = new TBarSeries();
// Add 5 bars
@@ -632,25 +653,31 @@ public class AccBandsTests
Assert.True(accBands.IsHot);
// Last 3 bars: H=[120,125,130], L=[100,105,110], C=[110,115,120]
// SMA(3) of High: (120+125+130)/3 = 125
// SMA(3) of Low: (100+105+110)/3 = 105
// SMA(3) of Close: (110+115+120)/3 = 115
// BandWidth = (125-105) * 2.0 = 40
// Last 3 bars: bars 2,3,4
// Bar 2: H=120,L=100,C=110 -> w=20/220, adjH=120*(1+80/220), adjL=100*(1-80/220)
// Bar 3: H=125,L=105,C=115 -> w=20/230, adjH=125*(1+80/230), adjL=105*(1-80/230)
// Bar 4: H=130,L=110,C=120 -> w=20/240, adjH=130*(1+80/240), adjL=110*(1-80/240)
double adjH2 = 120.0 * (1.0 + 4.0 * 20.0 / 220.0);
double adjL2 = 100.0 * (1.0 - 4.0 * 20.0 / 220.0);
double adjH3 = 125.0 * (1.0 + 4.0 * 20.0 / 230.0);
double adjL3 = 105.0 * (1.0 - 4.0 * 20.0 / 230.0);
double adjH4 = 130.0 * (1.0 + 4.0 * 20.0 / 240.0);
double adjL4 = 110.0 * (1.0 - 4.0 * 20.0 / 240.0);
Assert.Equal(115.0, accBands.Last.Value, 1e-10);
Assert.Equal(165.0, accBands.Upper.Value, 1e-10); // 125 + 40
Assert.Equal(65.0, accBands.Lower.Value, 1e-10); // 105 - 40
Assert.Equal((adjH2 + adjH3 + adjH4) / 3.0, accBands.Upper.Value, 1e-10);
Assert.Equal((adjL2 + adjL3 + adjL4) / 3.0, accBands.Lower.Value, 1e-10);
// Verify it continues correctly
accBands.Update(new TBar(DateTime.UtcNow, 125, 135, 115, 125, 1000));
// New window: H=[125,130,135], L=[105,110,115], C=[115,120,125]
// SMA(3) of High: (125+130+135)/3 = 130
// SMA(3) of Low: (105+110+115)/3 = 110
// SMA(3) of Close: (115+120+125)/3 = 120
// BandWidth = (130-110) * 2.0 = 40
// New window: bars [3,4,5]
// Bar 5: H=135,L=115,C=125 -> w=20/250, adjH=135*(1+80/250), adjL=115*(1-80/250)
double adjH5 = 135.0 * (1.0 + 4.0 * 20.0 / 250.0);
double adjL5 = 115.0 * (1.0 - 4.0 * 20.0 / 250.0);
Assert.Equal(120.0, accBands.Last.Value, 1e-10);
Assert.Equal(170.0, accBands.Upper.Value, 1e-10); // 130 + 40
Assert.Equal(70.0, accBands.Lower.Value, 1e-10); // 110 - 40
Assert.Equal((adjH3 + adjH4 + adjH5) / 3.0, accBands.Upper.Value, 1e-10);
Assert.Equal((adjL3 + adjL4 + adjL5) / 3.0, accBands.Lower.Value, 1e-10);
}
[Fact]
@@ -663,7 +690,7 @@ public class AccBandsTests
series.Add(DateTime.UtcNow, 115, 125, 105, 115, 1000);
series.Add(DateTime.UtcNow, 120, 130, 110, 120, 1000);
var ((middle, upper, lower), indicator) = AccBands.Calculate(series, 3, 2.0);
var ((middle, upper, lower), indicator) = AccBands.Calculate(series, 3, 4.0);
// Check results
Assert.Equal(5, middle.Count);
@@ -688,20 +715,18 @@ public class AccBandsTests
series.Add(DateTime.UtcNow, 105, 115, 95, 105, 1000);
series.Add(DateTime.UtcNow, 110, 120, 100, 110, 1000);
// Factor 1.0
var (middle1, upper1, lower1) = AccBands.Batch(series, 3, 1.0);
// SMA(3) High=115, Low=95, Close=105, BandWidth=20*1=20
Assert.Equal(135.0, upper1.Last.Value, 1e-10); // 115 + 20
Assert.Equal(75.0, lower1.Last.Value, 1e-10); // 95 - 20
// Factor 2.0
var (middle1, upper1, lower1) = AccBands.Batch(series, 3, 2.0);
// Factor 6.0
var (middle3, upper3, lower3) = AccBands.Batch(series, 3, 6.0);
// Factor 3.0
var (middle3, upper3, lower3) = AccBands.Batch(series, 3, 3.0);
// BandWidth=20*3=60
Assert.Equal(175.0, upper3.Last.Value, 1e-10); // 115 + 60
Assert.Equal(35.0, lower3.Last.Value, 1e-10); // 95 - 60
// Middle should be the same for all factors
// Middle should be the same regardless of factor (SMA of close)
Assert.Equal(middle1.Last.Value, middle3.Last.Value, 1e-10);
// Wider factor = wider bands
double width1 = upper1.Last.Value - lower1.Last.Value;
double width3 = upper3.Last.Value - lower3.Last.Value;
Assert.True(width3 > width1, $"Factor 6 width ({width3}) should be > factor 2 width ({width1})");
}
[Fact]
@@ -714,10 +739,11 @@ public class AccBandsTests
accBands.Update(new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1000));
}
// When H=L=C=100, BandWidth = (100-100)*2 = 0
// When H=L=C=100, w = (100-100)/(100+100) = 0
// adjHigh = 100*(1+0) = 100, adjLow = 100*(1-0) = 100
Assert.Equal(100.0, accBands.Last.Value, 1e-10);
Assert.Equal(100.0, accBands.Upper.Value, 1e-10); // 100 + 0
Assert.Equal(100.0, accBands.Lower.Value, 1e-10); // 100 - 0
Assert.Equal(100.0, accBands.Upper.Value, 1e-10);
Assert.Equal(100.0, accBands.Lower.Value, 1e-10);
}
[Fact]
@@ -5,10 +5,10 @@ namespace QuanTAlib.Tests;
/// <summary>
/// Validation tests for AccBands indicator.
/// Note: TA-Lib provides ACCBANDS but uses a different formula (per-bar adaptive width
/// via High*(1+4*(H-L)/(H+L))) whereas QuanTAlib uses SMA-based band width.
/// The middle band (SMA of Close) matches exactly between both implementations.
/// Skender, Tulip, and OoplesFinance do not provide AccBands.
/// Now using Headley's original formula: Upper = SMA(High*(1+factor*(H-L)/(H+L))),
/// Lower = SMA(Low*(1-factor*(H-L)/(H+L))), Middle = SMA(Close).
/// TA-Lib uses the same per-bar Headley formula with factor=4, so all three bands
/// should match exactly. Skender, Tulip, and OoplesFinance do not provide AccBands.
/// </summary>
public sealed class AccBandsValidationTests : IDisposable
{
@@ -45,15 +45,14 @@ public sealed class AccBandsValidationTests : IDisposable
[Fact]
public void Validate_ManualCalculation_Period3()
{
// Manual calculation verification
// Manual calculation verification with Headley's formula
// Given: High = [12, 14, 16], Low = [8, 10, 12], Close = [10, 12, 14]
// SMA(High, 3) = (12 + 14 + 16) / 3 = 14
// SMA(Low, 3) = (8 + 10 + 12) / 3 = 10
// SMA(Close, 3) = (10 + 12 + 14) / 3 = 12
// BandWidth = (14 - 10) * 2.0 = 8
// Upper = 14 + 8 = 22
// Lower = 10 - 8 = 2
// Middle = 12
// Bar 0: w=4/20=0.2, adjH=12*(1+4*0.2)=12*1.8=21.6, adjL=8*(1-4*0.2)=8*0.2=1.6
// Bar 1: w=4/24≈0.16667, adjH=14*(1+4/6)=14*1.66667≈23.33333, adjL=10*(1-4/6)=10*0.33333≈3.33333
// Bar 2: w=4/28≈0.14286, adjH=16*(1+4*4/28)=16*1.57143≈25.14286, adjL=12*(1-4*4/28)=12*0.42857≈5.14286
// SMA(3) Middle = (10+12+14)/3 = 12
// SMA(3) Upper = (21.6 + 23.33333 + 25.14286) / 3
// SMA(3) Lower = (1.6 + 3.33333 + 5.14286) / 3
var series = new TBarSeries();
var time = DateTime.UtcNow;
@@ -61,12 +60,19 @@ public sealed class AccBandsValidationTests : IDisposable
series.Add(new TBar(time.AddMinutes(1), 12, 14, 10, 12, 100));
series.Add(new TBar(time.AddMinutes(2), 14, 16, 12, 14, 100));
var accBands = new AccBands(3, 2.0);
var accBands = new AccBands(3, 4.0);
var (middle, upper, lower) = accBands.Update(series);
double adjH0 = 12.0 * (1.0 + 4.0 * 4.0 / 20.0);
double adjH1 = 14.0 * (1.0 + 4.0 * 4.0 / 24.0);
double adjH2 = 16.0 * (1.0 + 4.0 * 4.0 / 28.0);
double adjL0 = 8.0 * (1.0 - 4.0 * 4.0 / 20.0);
double adjL1 = 10.0 * (1.0 - 4.0 * 4.0 / 24.0);
double adjL2 = 12.0 * (1.0 - 4.0 * 4.0 / 28.0);
Assert.Equal(12.0, middle.Last.Value, 1e-10);
Assert.Equal(22.0, upper.Last.Value, 1e-10);
Assert.Equal(2.0, lower.Last.Value, 1e-10);
Assert.Equal((adjH0 + adjH1 + adjH2) / 3.0, upper.Last.Value, 1e-10);
Assert.Equal((adjL0 + adjL1 + adjL2) / 3.0, lower.Last.Value, 1e-10);
_output.WriteLine("AccBands manual calculation (period 3) validated successfully");
}
@@ -74,7 +80,7 @@ public sealed class AccBandsValidationTests : IDisposable
[Fact]
public void Validate_ManualCalculation_Period5()
{
// Manual calculation verification with period 5
// Manual calculation verification with period 5, Headley formula
var series = new TBarSeries();
var time = DateTime.UtcNow;
@@ -86,19 +92,25 @@ public sealed class AccBandsValidationTests : IDisposable
series.Add(new TBar(time.AddMinutes(i), c, c + 5, c - 5, c, 1000));
}
// SMA(High, 5) = (105 + 107 + 109 + 111 + 113) / 5 = 109
// SMA(Low, 5) = (95 + 97 + 99 + 101 + 103) / 5 = 99
// SMA(Close, 5) = (100 + 102 + 104 + 106 + 108) / 5 = 104
// BandWidth = (109 - 99) * 2.0 = 20
// Upper = 109 + 20 = 129
// Lower = 99 - 20 = 79
var accBands = new AccBands(5, 2.0);
var accBands = new AccBands(5, 4.0);
var (middle, upper, lower) = accBands.Update(series);
// SMA(Close, 5) = (100 + 102 + 104 + 106 + 108) / 5 = 104
Assert.Equal(104.0, middle.Last.Value, 1e-10);
Assert.Equal(129.0, upper.Last.Value, 1e-10);
Assert.Equal(79.0, lower.Last.Value, 1e-10);
// Each bar: H=c+5, L=c-5, w=10/(2c), adjH=(c+5)*(1+40/(2c)), adjL=(c-5)*(1-40/(2c))
double sumAdjH = 0, sumAdjL = 0;
foreach (double c in closes)
{
double h = c + 5;
double l = c - 5;
double denom = h + l;
double w = (h - l) / denom;
sumAdjH += h * (1.0 + 4.0 * w);
sumAdjL += l * (1.0 - 4.0 * w);
}
Assert.Equal(sumAdjH / 5.0, upper.Last.Value, 1e-10);
Assert.Equal(sumAdjL / 5.0, lower.Last.Value, 1e-10);
_output.WriteLine("AccBands manual calculation (period 5) validated successfully");
}
@@ -115,31 +127,28 @@ public sealed class AccBandsValidationTests : IDisposable
series.Add(new TBar(time.AddMinutes(i), 100, 110, 90, 100, 1000));
}
// With constant H/L/C: SMA(High)=110, SMA(Low)=90, SMA(Close)=100
// Spread = 110 - 90 = 20
// With constant H=110,L=90: w = 20/200 = 0.1 per bar
var (middle1, upper1, lower1) = AccBands.Batch(series, 5, 1.0);
var (middle2, upper2, lower2) = AccBands.Batch(series, 5, 2.0);
var (middle3, upper3, lower3) = AccBands.Batch(series, 5, 3.0);
var (middle1, upper1, lower1) = AccBands.Batch(series, 5, 2.0);
var (middle2, upper2, lower2) = AccBands.Batch(series, 5, 4.0);
var (middle3, upper3, lower3) = AccBands.Batch(series, 5, 6.0);
// Middle should be the same regardless of factor
// Middle should be the same regardless of factor (SMA of Close = 100)
Assert.Equal(middle1.Last.Value, middle2.Last.Value, 1e-10);
Assert.Equal(middle2.Last.Value, middle3.Last.Value, 1e-10);
Assert.Equal(100.0, middle1.Last.Value, 1e-10);
// BandWidth with factor 1.0 = 20
// BandWidth with factor 2.0 = 40
// BandWidth with factor 3.0 = 60
// factor=2: adjH=110*(1+2*0.1)=110*1.2=132, adjL=90*(1-2*0.1)=90*0.8=72
// factor=4: adjH=110*(1+4*0.1)=110*1.4=154, adjL=90*(1-4*0.1)=90*0.6=54
// factor=6: adjH=110*(1+6*0.1)=110*1.6=176, adjL=90*(1-6*0.1)=90*0.4=36
// Upper = SMA(High) + BandWidth
Assert.Equal(110.0 + 20.0, upper1.Last.Value, 1e-10); // 130
Assert.Equal(110.0 + 40.0, upper2.Last.Value, 1e-10); // 150
Assert.Equal(110.0 + 60.0, upper3.Last.Value, 1e-10); // 170
Assert.Equal(132.0, upper1.Last.Value, 1e-10);
Assert.Equal(154.0, upper2.Last.Value, 1e-10);
Assert.Equal(176.0, upper3.Last.Value, 1e-10);
// Lower = SMA(Low) - BandWidth
Assert.Equal(90.0 - 20.0, lower1.Last.Value, 1e-10); // 70
Assert.Equal(90.0 - 40.0, lower2.Last.Value, 1e-10); // 50
Assert.Equal(90.0 - 60.0, lower3.Last.Value, 1e-10); // 30
Assert.Equal(72.0, lower1.Last.Value, 1e-10);
Assert.Equal(54.0, lower2.Last.Value, 1e-10);
Assert.Equal(36.0, lower3.Last.Value, 1e-10);
_output.WriteLine("AccBands factor effect validated successfully");
}
@@ -152,11 +161,11 @@ public sealed class AccBandsValidationTests : IDisposable
foreach (var period in periods)
{
// Batch mode using instance
var accBands = new AccBands(period, 2.0);
var accBands = new AccBands(period, 4.0);
var (qMiddle, qUpper, qLower) = accBands.Update(_testData.Bars);
// Static batch
var (sMiddle, sUpper, sLower) = AccBands.Batch(_testData.Bars, period, 2.0);
var (sMiddle, sUpper, sLower) = AccBands.Batch(_testData.Bars, period, 4.0);
// Verify match
ValidationHelper.VerifySeriesEqual(qMiddle, sMiddle);
@@ -174,7 +183,7 @@ public sealed class AccBandsValidationTests : IDisposable
foreach (var period in periods)
{
// Streaming mode
var streamingAcc = new AccBands(period, 2.0);
var streamingAcc = new AccBands(period, 4.0);
var streamMiddle = new TSeries();
var streamUpper = new TSeries();
var streamLower = new TSeries();
@@ -187,7 +196,7 @@ public sealed class AccBandsValidationTests : IDisposable
}
// Batch mode for comparison
var (batchMiddle, batchUpper, batchLower) = AccBands.Batch(_testData.Bars, period, 2.0);
var (batchMiddle, batchUpper, batchLower) = AccBands.Batch(_testData.Bars, period, 4.0);
// Verify match
ValidationHelper.VerifySeriesEqual(batchMiddle, streamMiddle);
@@ -216,10 +225,10 @@ public sealed class AccBandsValidationTests : IDisposable
AccBands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(),
spanMiddle.AsSpan(), spanUpper.AsSpan(), spanLower.AsSpan(),
period, 2.0);
period, 4.0);
// Batch mode for comparison
var (batchMiddle, batchUpper, batchLower) = AccBands.Batch(_testData.Bars, period, 2.0);
var (batchMiddle, batchUpper, batchLower) = AccBands.Batch(_testData.Bars, period, 4.0);
// Verify match
for (int i = 0; i < len; i++)
@@ -241,7 +250,7 @@ public sealed class AccBandsValidationTests : IDisposable
{
// Eventing mode
var pubSource = new TBarSeries();
var eventingInd = new AccBands(pubSource, period, 2.0);
var eventingInd = new AccBands(pubSource, period, 4.0);
var eventMiddle = new TSeries();
var eventUpper = new TSeries();
var eventLower = new TSeries();
@@ -255,7 +264,7 @@ public sealed class AccBandsValidationTests : IDisposable
}
// Batch mode for comparison
var (batchMiddle, batchUpper, batchLower) = AccBands.Batch(_testData.Bars, period, 2.0);
var (batchMiddle, batchUpper, batchLower) = AccBands.Batch(_testData.Bars, period, 4.0);
// Verify match
ValidationHelper.VerifySeriesEqual(batchMiddle, eventMiddle);
@@ -272,7 +281,7 @@ public sealed class AccBandsValidationTests : IDisposable
foreach (var period in periods)
{
var ((middle, upper, lower), indicator) = AccBands.Calculate(_testData.Bars, period, 2.0);
var ((middle, upper, lower), indicator) = AccBands.Calculate(_testData.Bars, period, 4.0);
// Verify indicator is hot
Assert.True(indicator.IsHot);
@@ -295,7 +304,7 @@ public sealed class AccBandsValidationTests : IDisposable
public void Validate_LargeDataset_NoOverflow()
{
// Test with the full 5000 bar dataset
var (middle, upper, lower) = AccBands.Batch(_testData.Bars, 100, 2.0);
var (middle, upper, lower) = AccBands.Batch(_testData.Bars, 100, 4.0);
// All outputs should be finite
ValidationHelper.VerifyAllFinite(middle, startIndex: 0);
@@ -314,41 +323,6 @@ public sealed class AccBandsValidationTests : IDisposable
_output.WriteLine("AccBands large dataset (5000 bars) validated successfully");
}
[Fact]
public void Validate_BandWidth_IsSymmetric()
{
// Verify that Upper - SMA(High) == SMA(Low) - Lower
// This confirms the band width is applied symmetrically
var (middle, upper, lower) = AccBands.Batch(_testData.Bars, 20, 2.0);
// Calculate SMA(High) and SMA(Low) separately for verification
_ = middle; // Suppress unused variable warning - middle is not needed for symmetry test
var smaHigh = new Sma(20);
var smaLow = new Sma(20);
var smaHighResults = new TSeries();
var smaLowResults = new TSeries();
for (int i = 0; i < _testData.Bars.Count; i++)
{
var bar = _testData.Bars[i];
smaHighResults.Add(smaHigh.Update(new TValue(bar.Time, bar.High)));
smaLowResults.Add(smaLow.Update(new TValue(bar.Time, bar.Low)));
}
// After warmup, verify symmetry
for (int i = 20; i < _testData.Bars.Count; i++)
{
double upperDiff = upper[i].Value - smaHighResults[i].Value;
double lowerDiff = smaLowResults[i].Value - lower[i].Value;
Assert.Equal(upperDiff, lowerDiff, 1e-9);
}
_output.WriteLine("AccBands band width symmetry validated successfully");
}
[Fact]
public void Validate_Prime_ProducesCorrectState()
{
@@ -356,10 +330,10 @@ public sealed class AccBandsValidationTests : IDisposable
const int period = 20;
// Full batch calculation
var (batchMiddle, batchUpper, batchLower) = AccBands.Batch(_testData.Bars, period, 2.0);
var (batchMiddle, batchUpper, batchLower) = AccBands.Batch(_testData.Bars, period, 4.0);
// Prime indicator with subset and continue
var primedIndicator = new AccBands(period, 2.0);
var primedIndicator = new AccBands(period, 4.0);
var subset = new TBarSeries();
for (int i = 0; i < 100; i++)
{
@@ -382,10 +356,13 @@ public sealed class AccBandsValidationTests : IDisposable
}
[Fact]
public void Validate_Talib_MiddleBand_Batch()
public void Validate_Talib_AllBands_Batch()
{
// TALib ACCBANDS uses a different upper/lower formula (per-bar adaptive width via
// High*(1+4*(H-L)/(H+L))) but the MIDDLE band is SMA(Close) which matches exactly.
// TA-Lib ACCBANDS uses the same Headley formula:
// Upper = SMA(High*(1+4*(H-L)/(H+L)), period)
// Lower = SMA(Low*(1-4*(H-L)/(H+L)), period)
// Middle = SMA(Close, period)
// Now all three bands should match exactly.
int[] periods = { 5, 10, 20, 50, 100 };
double[] high = _testData.HighPrices.ToArray();
@@ -399,8 +376,8 @@ public sealed class AccBandsValidationTests : IDisposable
foreach (var period in periods)
{
// QuanTAlib AccBands (batch)
var (qMiddle, _, _) = AccBands.Batch(_testData.Bars, period, 2.0);
// QuanTAlib AccBands (batch) with factor=4 to match TA-Lib default
var (qMiddle, qUpper, qLower) = AccBands.Batch(_testData.Bars, period, 4.0);
// TALib Accbands
var retCode = Functions.Accbands<double>(
@@ -414,16 +391,18 @@ public sealed class AccBandsValidationTests : IDisposable
int lookback = Functions.AccbandsLookback(period);
// Middle band = SMA(Close) in both implementations — should match exactly
// All three bands should match (same Headley formula)
ValidationHelper.VerifyData(qMiddle, talibMiddle, outRange, lookback);
ValidationHelper.VerifyData(qUpper, talibUpper, outRange, lookback);
ValidationHelper.VerifyData(qLower, talibLower, outRange, lookback);
}
_output.WriteLine("AccBands middle band validated successfully against TA-Lib");
_output.WriteLine("AccBands all bands validated successfully against TA-Lib");
}
[Fact]
public void Validate_Talib_MiddleBand_Span()
public void Validate_Talib_AllBands_Span()
{
// Validate middle band match using Span API
// Validate all band match using Span API
int[] periods = { 5, 10, 20, 50, 100 };
double[] high = _testData.HighPrices.ToArray();
@@ -437,13 +416,13 @@ public sealed class AccBandsValidationTests : IDisposable
foreach (var period in periods)
{
// QuanTAlib AccBands (Span API)
// QuanTAlib AccBands (Span API) with factor=4
double[] qMiddle = new double[len];
double[] qUpper = new double[len];
double[] qLower = new double[len];
AccBands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(),
qMiddle.AsSpan(), qUpper.AsSpan(), qLower.AsSpan(),
period, 2.0);
period, 4.0);
// TALib Accbands
var retCode = Functions.Accbands<double>(
@@ -457,20 +436,18 @@ public sealed class AccBandsValidationTests : IDisposable
int lookback = Functions.AccbandsLookback(period);
// Middle band = SMA(Close) — exact match
// All three bands should match
ValidationHelper.VerifyData(qMiddle, talibMiddle, outRange, lookback);
ValidationHelper.VerifyData(qUpper, talibUpper, outRange, lookback);
ValidationHelper.VerifyData(qLower, talibLower, outRange, lookback);
}
_output.WriteLine("AccBands Span middle band validated successfully against TA-Lib");
_output.WriteLine("AccBands Span all bands validated successfully against TA-Lib");
}
[Fact]
public void Validate_Talib_FormulaConventionDifference()
public void Validate_Talib_StructuralRelationships()
{
// Document and verify that upper/lower bands differ between implementations.
// TALib: Upper = SMA(High * (1 + 4*(H-L)/(H+L))), per-bar adaptive width
// QuanTAlib: Upper = SMA(High) + factor*(SMA(High)-SMA(Low)), SMA-based width
// Both are valid "Acceleration Bands" variants.
// Verify structural relationships hold for both implementations
const int period = 20;
double[] high = _testData.HighPrices.ToArray();
@@ -491,29 +468,18 @@ public sealed class AccBandsValidationTests : IDisposable
Assert.Equal(Core.RetCode.Success, retCode);
var (qMiddle, qUpper, qLower) = AccBands.Batch(_testData.Bars, period, 2.0);
var (qMiddle, qUpper, qLower) = AccBands.Batch(_testData.Bars, period, 4.0);
int lookback = Functions.AccbandsLookback(period);
int talibStart = outRange.Start.Value;
// Middle bands should match (both SMA of Close)
for (int i = lookback; i < qMiddle.Count && (i - talibStart) < len; i++)
{
int tIdx = i - talibStart;
if (tIdx >= 0 && tIdx < len && talibMiddle[tIdx] != 0)
{
Assert.Equal(qMiddle[i].Value, talibMiddle[tIdx], 1e-7);
}
}
// Upper/Lower bands should differ (different formulas) but maintain same structure
// Both should have Upper > Middle > Lower
int structuralCount = 0;
for (int i = lookback; i < qMiddle.Count && (i - talibStart) < len; i++)
{
int tIdx = i - talibStart;
if (tIdx >= 0 && tIdx < len && talibUpper[tIdx] != 0)
{
// Both should have Upper > Middle > Lower
Assert.True(qUpper[i].Value > qMiddle[i].Value, $"Q: Upper > Middle at {i}");
Assert.True(qLower[i].Value < qMiddle[i].Value, $"Q: Lower < Middle at {i}");
Assert.True(talibUpper[tIdx] > talibMiddle[tIdx], $"TALib: Upper > Middle at {i}");
@@ -523,6 +489,6 @@ public sealed class AccBandsValidationTests : IDisposable
}
Assert.True(structuralCount > 100, $"Validated {structuralCount} bars structurally");
_output.WriteLine($"AccBands formula convention difference validated ({structuralCount} bars)");
_output.WriteLine($"AccBands structural relationships validated ({structuralCount} bars)");
}
}
+107 -80
View File
@@ -9,30 +9,30 @@ namespace QuanTAlib;
/// </summary>
/// <remarks>
/// Acceleration Bands are a volatility-based channel indicator developed by Price Headley.
/// They create an adaptive price envelope around a moving average, with band width determined
/// by the spread between the high and low moving averages multiplied by a factor.
/// They create an adaptive price envelope around a moving average, where the band width
/// is determined by the per-bar normalized range applied before averaging.
///
/// Calculation:
/// Calculation (Headley's original formula):
/// w = (High - Low) / (High + Low) // normalized range width per bar
/// Upper Band = SMA(High × (1 + factor × w), Period)
/// Lower Band = SMA(Low × (1 - factor × w), Period)
/// Middle Band = SMA(Close, Period)
/// BandWidth = [SMA(High, Period) - SMA(Low, Period)] × Factor
/// Upper Band = SMA(High, Period) + BandWidth
/// Lower Band = SMA(Low, Period) - BandWidth
///
/// Key characteristics:
/// - Width adjustment is applied per bar before averaging (Headley's method)
/// - Bands expand during volatile periods and contract during consolidation
/// - Uses SMA of High, Low, and Close for calculations
/// - Factor parameter controls band sensitivity
/// - Factor parameter (default 4.0) controls band sensitivity
///
/// Sources:
/// Headley, P. (2002). Big Trends in Trading. John Wiley & Sons.
/// Headley, P. (2002). Big Trends in Trading. John Wiley &amp; Sons.
/// </remarks>
[SkipLocalsInit]
public sealed class AccBands : ITValuePublisher, IDisposable
{
private readonly int _period;
private readonly double _factor;
private readonly RingBuffer _highBuffer;
private readonly RingBuffer _lowBuffer;
private readonly RingBuffer _adjHighBuffer;
private readonly RingBuffer _adjLowBuffer;
private readonly RingBuffer _closeBuffer;
private readonly TBarPublishedHandler _barHandler;
private TBarSeries? _source;
@@ -42,8 +42,8 @@ public sealed class AccBands : ITValuePublisher, IDisposable
[StructLayout(LayoutKind.Auto)]
private record struct State(
double SumHigh,
double SumLow,
double SumAdjHigh,
double SumAdjLow,
double SumClose,
double LastValidHigh,
double LastValidLow,
@@ -92,8 +92,8 @@ public sealed class AccBands : ITValuePublisher, IDisposable
/// Creates AccBands with specified period and factor.
/// </summary>
/// <param name="period">Lookback period for SMA calculations (must be > 0)</param>
/// <param name="factor">Multiplier for band width (must be > 0, default: 2.0)</param>
public AccBands(int period, double factor = 2.0)
/// <param name="factor">Multiplier for normalized width (must be > 0, default: 4.0 per Headley)</param>
public AccBands(int period, double factor = 4.0)
{
if (period <= 0)
{
@@ -107,8 +107,8 @@ public sealed class AccBands : ITValuePublisher, IDisposable
_period = period;
_factor = factor;
_highBuffer = new RingBuffer(period);
_lowBuffer = new RingBuffer(period);
_adjHighBuffer = new RingBuffer(period);
_adjLowBuffer = new RingBuffer(period);
_closeBuffer = new RingBuffer(period);
Name = $"AccBands({period},{factor:F2})";
WarmupPeriod = period;
@@ -118,7 +118,7 @@ public sealed class AccBands : ITValuePublisher, IDisposable
/// <summary>
/// Creates AccBands with TBarSeries source.
/// </summary>
public AccBands(TBarSeries source, int period, double factor = 2.0) : this(period, factor)
public AccBands(TBarSeries source, int period, double factor = 4.0) : this(period, factor)
{
_source = source;
Prime(source);
@@ -191,27 +191,36 @@ public sealed class AccBands : ITValuePublisher, IDisposable
return _state.LastValidClose;
}
/// <summary>
/// Computes Headley's per-bar adjusted values and updates running sums.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UpdateState(double high, double low, double close)
{
double removedHigh = _highBuffer.Count == _highBuffer.Capacity ? _highBuffer.Oldest : 0.0;
double removedLow = _lowBuffer.Count == _lowBuffer.Capacity ? _lowBuffer.Oldest : 0.0;
// Headley's per-bar normalized width
double denom = high + low;
double w = denom != 0.0 ? (high - low) / denom : 0.0;
double adjHigh = high * (1.0 + _factor * w);
double adjLow = low * (1.0 - _factor * w);
double removedAdjHigh = _adjHighBuffer.Count == _adjHighBuffer.Capacity ? _adjHighBuffer.Oldest : 0.0;
double removedAdjLow = _adjLowBuffer.Count == _adjLowBuffer.Capacity ? _adjLowBuffer.Oldest : 0.0;
double removedClose = _closeBuffer.Count == _closeBuffer.Capacity ? _closeBuffer.Oldest : 0.0;
_state.SumHigh = _state.SumHigh - removedHigh + high;
_state.SumLow = _state.SumLow - removedLow + low;
_state.SumAdjHigh = _state.SumAdjHigh - removedAdjHigh + adjHigh;
_state.SumAdjLow = _state.SumAdjLow - removedAdjLow + adjLow;
_state.SumClose = _state.SumClose - removedClose + close;
_highBuffer.Add(high);
_lowBuffer.Add(low);
_adjHighBuffer.Add(adjHigh);
_adjLowBuffer.Add(adjLow);
_closeBuffer.Add(close);
_state.TickCount++;
if (_closeBuffer.IsFull && _state.TickCount >= ResyncInterval)
{
_state.TickCount = 0;
_state.SumHigh = _highBuffer.RecalculateSum();
_state.SumLow = _lowBuffer.RecalculateSum();
_state.SumAdjHigh = _adjHighBuffer.RecalculateSum();
_state.SumAdjLow = _adjLowBuffer.RecalculateSum();
_state.SumClose = _closeBuffer.RecalculateSum();
}
}
@@ -239,14 +248,20 @@ public sealed class AccBands : ITValuePublisher, IDisposable
double low = GetValidLow(input.Low);
double close = GetValidClose(input.Close);
_highBuffer.UpdateNewest(high);
_lowBuffer.UpdateNewest(low);
// Recompute adjusted values for the corrected bar
double denom = high + low;
double w = denom != 0.0 ? (high - low) / denom : 0.0;
double adjHigh = high * (1.0 + _factor * w);
double adjLow = low * (1.0 - _factor * w);
_adjHighBuffer.UpdateNewest(adjHigh);
_adjLowBuffer.UpdateNewest(adjLow);
_closeBuffer.UpdateNewest(close);
_state = _state with
{
SumHigh = _highBuffer.Sum,
SumLow = _lowBuffer.Sum,
SumAdjHigh = _adjHighBuffer.Sum,
SumAdjLow = _adjLowBuffer.Sum,
SumClose = _closeBuffer.Sum,
};
}
@@ -260,14 +275,13 @@ public sealed class AccBands : ITValuePublisher, IDisposable
}
else
{
double smaHigh = _state.SumHigh / count;
double smaLow = _state.SumLow / count;
double smaAdjHigh = _state.SumAdjHigh / count;
double smaAdjLow = _state.SumAdjLow / count;
double smaClose = _state.SumClose / count;
double bandWidth = (smaHigh - smaLow) * _factor;
Last = new TValue(input.Time, smaClose);
Upper = new TValue(input.Time, smaHigh + bandWidth);
Lower = new TValue(input.Time, smaLow - bandWidth);
Upper = new TValue(input.Time, smaAdjHigh);
Lower = new TValue(input.Time, smaAdjLow);
}
PubEvent(Last, isNew);
@@ -332,8 +346,8 @@ public sealed class AccBands : ITValuePublisher, IDisposable
}
// Reset state
_highBuffer.Clear();
_lowBuffer.Clear();
_adjHighBuffer.Clear();
_adjLowBuffer.Clear();
_closeBuffer.Clear();
_state = default;
_p_state = default;
@@ -413,14 +427,13 @@ public sealed class AccBands : ITValuePublisher, IDisposable
if (count > 0)
{
var lastBar = source.Last;
double smaHigh = _state.SumHigh / count;
double smaLow = _state.SumLow / count;
double smaAdjHigh = _state.SumAdjHigh / count;
double smaAdjLow = _state.SumAdjLow / count;
double smaClose = _state.SumClose / count;
double bandWidth = (smaHigh - smaLow) * _factor;
Last = new TValue(lastBar.Time, smaClose);
Upper = new TValue(lastBar.Time, smaHigh + bandWidth);
Lower = new TValue(lastBar.Time, smaLow - bandWidth);
Upper = new TValue(lastBar.Time, smaAdjHigh);
Lower = new TValue(lastBar.Time, smaAdjLow);
}
_p_state = _state;
@@ -431,12 +444,12 @@ public sealed class AccBands : ITValuePublisher, IDisposable
/// </summary>
public void Reset()
{
_highBuffer.Clear();
_lowBuffer.Clear();
_adjHighBuffer.Clear();
_adjLowBuffer.Clear();
_closeBuffer.Clear();
_state = new State(
SumHigh: 0,
SumLow: 0,
SumAdjHigh: 0,
SumAdjLow: 0,
SumClose: 0,
LastValidHigh: double.NaN,
LastValidLow: double.NaN,
@@ -515,8 +528,8 @@ public sealed class AccBands : ITValuePublisher, IDisposable
[StructLayout(LayoutKind.Auto)]
private ref struct ScalarState
{
public double SumHigh;
public double SumLow;
public double SumAdjHigh;
public double SumAdjLow;
public double SumClose;
public double LastValidHigh;
public double LastValidLow;
@@ -529,17 +542,17 @@ public sealed class AccBands : ITValuePublisher, IDisposable
/// Working buffers for batch calculation.
/// </summary>
[StructLayout(LayoutKind.Auto)]
private readonly ref struct WorkBuffers(Span<double> high, Span<double> low, Span<double> close)
private readonly ref struct WorkBuffers(Span<double> adjHigh, Span<double> adjLow, Span<double> close)
{
public readonly Span<double> High = high;
public readonly Span<double> Low = low;
public readonly Span<double> AdjHigh = adjHigh;
public readonly Span<double> AdjLow = adjLow;
public readonly Span<double> Close = close;
}
/// <summary>
/// Calculates AccBands for the entire TBarSeries using a new instance.
/// </summary>
public static (TSeries Middle, TSeries Upper, TSeries Lower) Batch(TBarSeries source, int period, double factor = 2.0)
public static (TSeries Middle, TSeries Upper, TSeries Lower) Batch(TBarSeries source, int period, double factor = 4.0)
{
var accBands = new AccBands(period, factor);
return accBands.Update(source);
@@ -558,7 +571,7 @@ public sealed class AccBands : ITValuePublisher, IDisposable
BatchInputs inputs,
BatchOutputs outputs,
int period,
double factor = 2.0)
double factor = 4.0)
{
Batch(inputs.High, inputs.Low, inputs.Close, outputs.Middle, outputs.Upper, outputs.Lower, period, factor);
}
@@ -580,7 +593,7 @@ public sealed class AccBands : ITValuePublisher, IDisposable
ReadOnlySpan<double> close,
BatchOutputs outputs,
int period,
double factor = 2.0)
double factor = 4.0)
{
Batch(high, low, close, outputs.Middle, outputs.Upper, outputs.Lower, period, factor);
}
@@ -609,7 +622,7 @@ public sealed class AccBands : ITValuePublisher, IDisposable
Span<double> upper,
Span<double> lower,
int period,
double factor = 2.0)
double factor = 4.0)
#pragma warning restore S107
{
int len = close.Length;
@@ -654,15 +667,15 @@ public sealed class AccBands : ITValuePublisher, IDisposable
int len = inputs.Close.Length;
// Always use ArrayPool to avoid span scope safety issues with stackalloc + ref structs
double[] rentedHigh = ArrayPool<double>.Shared.Rent(period);
double[] rentedLow = ArrayPool<double>.Shared.Rent(period);
double[] rentedAdjHigh = ArrayPool<double>.Shared.Rent(period);
double[] rentedAdjLow = ArrayPool<double>.Shared.Rent(period);
double[] rentedClose = ArrayPool<double>.Shared.Rent(period);
try
{
var buffers = new WorkBuffers(
rentedHigh.AsSpan(0, period),
rentedLow.AsSpan(0, period),
rentedAdjHigh.AsSpan(0, period),
rentedAdjLow.AsSpan(0, period),
rentedClose.AsSpan(0, period));
var state = new ScalarState
@@ -680,8 +693,8 @@ public sealed class AccBands : ITValuePublisher, IDisposable
}
finally
{
ArrayPool<double>.Shared.Return(rentedHigh);
ArrayPool<double>.Shared.Return(rentedLow);
ArrayPool<double>.Shared.Return(rentedAdjHigh);
ArrayPool<double>.Shared.Return(rentedAdjLow);
ArrayPool<double>.Shared.Return(rentedClose);
}
}
@@ -751,13 +764,15 @@ public sealed class AccBands : ITValuePublisher, IDisposable
return (h, l, c);
}
/// <summary>
/// Computes adjusted high/low per bar using Headley's formula and writes band outputs.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteBandOutputs(scoped BatchOutputs outputs, int i, double smaHigh, double smaLow, double smaClose, double factor)
private static void WriteBandOutputs(scoped BatchOutputs outputs, int i, double smaAdjHigh, double smaAdjLow, double smaClose)
{
double bandWidth = (smaHigh - smaLow) * factor;
outputs.Middle[i] = smaClose;
outputs.Upper[i] = smaHigh + bandWidth;
outputs.Lower[i] = smaLow - bandWidth;
outputs.Upper[i] = smaAdjHigh;
outputs.Lower[i] = smaAdjLow;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -773,16 +788,22 @@ public sealed class AccBands : ITValuePublisher, IDisposable
{
var (h, l, c) = GetValidHLC(inputs, i, ref state);
state.SumHigh += h;
state.SumLow += l;
// Headley's per-bar adjustment
double denom = h + l;
double w = denom != 0.0 ? (h - l) / denom : 0.0;
double adjHigh = h * (1.0 + factor * w);
double adjLow = l * (1.0 - factor * w);
state.SumAdjHigh += adjHigh;
state.SumAdjLow += adjLow;
state.SumClose += c;
buffers.High[i] = h;
buffers.Low[i] = l;
buffers.AdjHigh[i] = adjHigh;
buffers.AdjLow[i] = adjLow;
buffers.Close[i] = c;
int count = i + 1;
WriteBandOutputs(outputs, i, state.SumHigh / count, state.SumLow / count, state.SumClose / count, factor);
WriteBandOutputs(outputs, i, state.SumAdjHigh / count, state.SumAdjLow / count, state.SumClose / count);
}
}
@@ -801,12 +822,18 @@ public sealed class AccBands : ITValuePublisher, IDisposable
{
var (h, l, c) = GetValidHLC(inputs, i, ref state);
state.SumHigh = state.SumHigh - buffers.High[state.BufferIndex] + h;
state.SumLow = state.SumLow - buffers.Low[state.BufferIndex] + l;
// Headley's per-bar adjustment
double denom = h + l;
double w = denom != 0.0 ? (h - l) / denom : 0.0;
double adjHigh = h * (1.0 + factor * w);
double adjLow = l * (1.0 - factor * w);
state.SumAdjHigh = state.SumAdjHigh - buffers.AdjHigh[state.BufferIndex] + adjHigh;
state.SumAdjLow = state.SumAdjLow - buffers.AdjLow[state.BufferIndex] + adjLow;
state.SumClose = state.SumClose - buffers.Close[state.BufferIndex] + c;
buffers.High[state.BufferIndex] = h;
buffers.Low[state.BufferIndex] = l;
buffers.AdjHigh[state.BufferIndex] = adjHigh;
buffers.AdjLow[state.BufferIndex] = adjLow;
buffers.Close[state.BufferIndex] = c;
state.BufferIndex++;
@@ -815,7 +842,7 @@ public sealed class AccBands : ITValuePublisher, IDisposable
state.BufferIndex = 0;
}
WriteBandOutputs(outputs, i, state.SumHigh / period, state.SumLow / period, state.SumClose / period, factor);
WriteBandOutputs(outputs, i, state.SumAdjHigh / period, state.SumAdjLow / period, state.SumClose / period);
state.TickCount++;
if (state.TickCount >= ResyncInterval)
@@ -829,18 +856,18 @@ public sealed class AccBands : ITValuePublisher, IDisposable
private static void ResyncSums(int period, ref WorkBuffers buffers, ref ScalarState state)
{
state.TickCount = 0;
ReadOnlySpan<double> highSpan = buffers.High[..period];
ReadOnlySpan<double> lowSpan = buffers.Low[..period];
ReadOnlySpan<double> adjHighSpan = buffers.AdjHigh[..period];
ReadOnlySpan<double> adjLowSpan = buffers.AdjLow[..period];
ReadOnlySpan<double> closeSpan = buffers.Close[..period];
state.SumHigh = highSpan.SumSIMD();
state.SumLow = lowSpan.SumSIMD();
state.SumAdjHigh = adjHighSpan.SumSIMD();
state.SumAdjLow = adjLowSpan.SumSIMD();
state.SumClose = closeSpan.SumSIMD();
}
/// <summary>
/// Runs a high-performance batch calculation and returns a "Hot" AccBands instance.
/// </summary>
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, AccBands Indicator) Calculate(TBarSeries source, int period, double factor = 2.0)
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, AccBands Indicator) Calculate(TBarSeries source, int period, double factor = 4.0)
{
var accBands = new AccBands(period, factor);
var results = accBands.Update(source);
+25 -26
View File
@@ -6,45 +6,44 @@ Acceleration Bands (ACCBANDS) serve as an adaptive volatility envelope based on
## Historical Context
Developed by Price Headley and detailed in *Big Trends in Trading* (2002), Acceleration Bands addressed the need for a breakout-specific envelope. Headley observed that standard deviation often lagged in fast-moving breakout scenarios. By incorporating the High and Low prices directly into the band width calculation, he created a system that reacts immediately to range expansion, often serving as a trigger for trend-following entries when price closes outside the bands.
Developed by Price Headley and detailed in *Big Trends in Trading* (2002), Acceleration Bands addressed the need for a breakout-specific envelope. Headley observed that standard deviation often lagged in fast-moving breakout scenarios. By incorporating the High and Low prices directly into the band width calculation — using a per-bar normalized range width — he created a system that reacts immediately to range expansion, often serving as a trigger for trend-following entries when price closes outside the bands.
## Architecture & Physics
The indicator maintains three parallel Simple Moving Averages (High, Low, and Close) to construct the bands. The width is derived from the smoothed High-Low range, scaled by a user-defined factor.
The indicator applies a per-bar width adjustment based on the normalized range `w = (H-L)/(H+L)` before averaging. This means wider-range bars contribute proportionally more to band expansion. Three Simple Moving Averages (adjusted high, adjusted low, close) construct the bands.
### Calculation Steps
### Calculation Steps (Headley's Formula)
1. **Component SMAs**:
$$SMA_{High} = \frac{1}{n} \sum_{i=0}^{n-1} \text{High}_{t-i}$$
$$SMA_{Low} = \frac{1}{n} \sum_{i=0}^{n-1} \text{Low}_{t-i}$$
$$SMA_{Close} = \frac{1}{n} \sum_{i=0}^{n-1} \text{Close}_{t-i}$$
1. **Per-bar normalized width**:
$$w_t = \frac{High_t - Low_t}{High_t + Low_t}$$
2. **Band Width**:
$$Width_t = (SMA_{High} - SMA_{Low}) \times Factor$$
2. **Adjusted prices per bar**:
$$AdjHigh_t = High_t \times (1 + Factor \times w_t)$$
$$AdjLow_t = Low_t \times (1 - Factor \times w_t)$$
3. **Band Construction**:
$$Upper_t = SMA_{High} + Width_t$$
$$Lower_t = SMA_{Low} - Width_t$$
$$Middle_t = SMA_{Close}$$
$$Upper_t = SMA(AdjHigh, n)$$
$$Lower_t = SMA(AdjLow, n)$$
$$Middle_t = SMA(Close, n)$$
Where $n$ = period (default 20), $Factor$ = multiplier (default 2.0).
Where $n$ = period (default 20), $Factor$ = multiplier (default 4.0).
## Performance Profile
The implementation uses three independent circular buffers (High, Low, Close) to maintain O(1) complexity for the moving averages.
The implementation uses three independent circular buffers (adjusted high, adjusted low, close) to maintain O(1) complexity for the moving averages.
### Operation Count - Single value
| Operation | Count | Cost (cycles) | Subtotal |
| :--- | :---: | :---: | :---: |
| ADD/SUB | 8 | 1 | 8 |
| MUL | 2 | 3 | 6 |
| DIV | 3 | 15 | 45 |
| **Total** | **13** | — | **~59 cycles** |
| ADD/SUB | 10 | 1 | 10 |
| MUL | 4 | 3 | 12 |
| DIV | 4 | 15 | 60 |
| **Total** | **18** | — | **~82 cycles** |
### Operation Count - Batch processing
SIMD optimization is applied to the final band construction, though the recursive nature of the SMAs limits full vectorization of the state maintenance.
SIMD optimization is applied to the sum resynchronization, though the recursive nature of the SMAs limits full vectorization of the state maintenance.
| Operation | Scalar Ops | SIMD Ops (AVX/SSE) | Acceleration |
| :--- | :---: | :---: | :---: |
@@ -55,16 +54,16 @@ SIMD optimization is applied to the final band construction, though the recursiv
| Library | Status | Notes |
| :--- | :--- | :--- |
| **TA-Lib** | N/A | Not implemented |
| **Skender** | ✅ | Matches `getAccelerationBands` |
| **TA-Lib** | ✅ | All three bands match exactly (same Headley formula) |
| **Internal** | ✅ | Streaming/Batch/Span match exactly |
## Usage & Pitfalls
- **Trend Definition**: Headley defines a breakout as two consecutive closes outside the bands.
- **Parameter Sensitivity**: The default factor of 2.0 is tuned for equities. Crypto or FX may require higher factors (e.g., 3.0) due to "fat tails" in intra-bar range.
- **Parameter Sensitivity**: The default factor of 4.0 matches TA-Lib and Headley's original. Lower factors (e.g., 2.0) produce tighter bands; higher factors (e.g., 6.0) may be needed for crypto/FX.
- **Lag**: Inherits the lag of the underlying SMA. Not suitable for ultra-high-frequency reacting.
- **Range vs Variance**: Because it uses High-Low range, it is more sensitive to "wicks" or momentary spikes than close-based envelopes.
- **Division by Zero**: When High + Low = 0 (price is zero), the normalized width defaults to 0.
## API
@@ -87,14 +86,14 @@ classDiagram
| Parameter | Type | Default | Range | Description |
| :--- | :--- | :--- | :--- | :--- |
| `period` | `int` | — | `>0` | Lookback period for SMAs. |
| `factor` | `double` | `2.0` | `>0` | Multiplier for band width. |
| `factor` | `double` | `4.0` | `>0` | Multiplier for normalized width. |
| `source` | `TBarSeries` | — | `any` | Initial input TBar data (optional). |
### Properties
- `Last` (`TValue`): The current middle band value (SMA of Close).
- `Upper` (`TValue`): The current upper band value.
- `Lower` (`TValue`): The current lower band value.
- `Upper` (`TValue`): The current upper band value (SMA of adjusted High).
- `Lower` (`TValue`): The current lower band value (SMA of adjusted Low).
- `IsHot` (`bool`): Returns `true` if valid data is available (warmup complete).
### Methods
@@ -109,7 +108,7 @@ classDiagram
using QuanTAlib;
// Initialize
var indicator = new AccBands(period: 20, factor: 2.0);
var indicator = new AccBands(period: 20, factor: 4.0);
// Update Loop
foreach (var bar in bars)
+25 -21
View File
@@ -3,56 +3,60 @@
//@version=6
indicator("Acceleration Bands (ACCBANDS)", "ACCBANDS", overlay=true)
//@function Calculates Acceleration Bands using SMAs of high, low, close prices
//@function Calculates Acceleration Bands using Price Headley's original formula
//@param high Series of high prices
//@param low Series of low prices
//@param close Series of close prices
//@param period Lookback period for the moving average
//@param factor Multiplier for band width calculation
//@param factor Multiplier for normalized width (default 4.0 per Headley)
//@returns tuple with [middle, upper, lower] band values
//@optimized Uses circular buffers with O(1) complexity per bar
accbands(series float high, series float low, series float close, simple int period, simple float factor = 2.0) =>
accbands(series float high, series float low, series float close, simple int period, simple float factor = 4.0) =>
if period <= 0 or factor <= 0.0
runtime.error("Period and factor must be greater than 0")
var int p = math.max(1, period)
var int head = 0
var int count = 0
var array<float> bufferHigh = array.new_float(p, na)
var array<float> bufferLow = array.new_float(p, na)
var array<float> bufferAdjHigh = array.new_float(p, na)
var array<float> bufferAdjLow = array.new_float(p, na)
var array<float> bufferClose = array.new_float(p, na)
var float sumHigh = 0.0
var float sumLow = 0.0
var float sumAdjHigh = 0.0
var float sumAdjLow = 0.0
var float sumClose = 0.0
float oldestHigh = array.get(bufferHigh, head)
float oldestLow = array.get(bufferLow, head)
float oldestAdjHigh = array.get(bufferAdjHigh, head)
float oldestAdjLow = array.get(bufferAdjLow, head)
float oldestClose = array.get(bufferClose, head)
if not na(oldestHigh)
sumHigh -= oldestHigh
sumLow -= oldestLow
if not na(oldestAdjHigh)
sumAdjHigh -= oldestAdjHigh
sumAdjLow -= oldestAdjLow
sumClose -= oldestClose
count -= 1
float currentHigh = nz(high)
float currentLow = nz(low)
float currentClose = nz(close)
sumHigh += currentHigh
sumLow += currentLow
// Headley's per-bar normalized width
float denom = currentHigh + currentLow
float w = denom != 0.0 ? (currentHigh - currentLow) / denom : 0.0
float adjHigh = currentHigh * (1.0 + factor * w)
float adjLow = currentLow * (1.0 - factor * w)
sumAdjHigh += adjHigh
sumAdjLow += adjLow
sumClose += currentClose
count += 1
array.set(bufferHigh, head, currentHigh)
array.set(bufferLow, head, currentLow)
array.set(bufferAdjHigh, head, adjHigh)
array.set(bufferAdjLow, head, adjLow)
array.set(bufferClose, head, currentClose)
head := (head + 1) % p
float smaHigh = nz(sumHigh / count)
float smaLow = nz(sumLow / count)
float smaAdjHigh = nz(sumAdjHigh / count)
float smaAdjLow = nz(sumAdjLow / count)
float smaClose = nz(sumClose / count)
float bandWidth = (smaHigh - smaLow) * factor
[smaClose, smaHigh + bandWidth, smaLow - bandWidth]
[smaClose, smaAdjHigh, smaAdjLow]
// ---------- Main loop ----------
// Inputs
i_period = input.int(20, "Period", minval=1)
i_factor = input.float(2.0, "Factor", minval=0.001)
i_factor = input.float(4.0, "Factor", minval=0.001)
// Calculation
[middle, upper, lower] = accbands(high, low, close, i_period, i_factor)