mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 13:58:04 +00:00
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:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 & 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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user