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
+9 -10
View File
@@ -144,28 +144,27 @@ public class AbberTests
var abber = new Abber(3, 2.0);
// Bar 1: source = 100
// SMA = 100, Deviation = 0, AvgDev = 0
// SMA = 100, Deviation = |100-100| = 0, AvgDev = 0
abber.Update(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100.0, abber.Last.Value, 1e-10);
// Bar 2: source = 110
// SMA(2) = (100+110)/2 = 105
// Dev1 = |100 - 100| = 0 (calculated when 100 was added, SMA was 100)
// Dev2 = |110 - 100| = 10 (calculated when 110 is added, SMA was 100)
// AvgDev = (0+10)/2 = 5
// Upper = 105 + 2*5 = 115, Lower = 105 - 2*5 = 95
// Dev1 = 0, Dev2 = |110 - 105| = 5 (same-bar SMA)
// AvgDev = (0+5)/2 = 2.5
// Upper = 105 + 2*2.5 = 110, Lower = 105 - 2*2.5 = 100
abber.Update(new TValue(DateTime.UtcNow, 110));
Assert.Equal(105.0, abber.Last.Value, 1e-10);
// Bar 3: source = 120
// SMA(3) = (100+110+120)/3 = 110
// Dev3 = |120 - 105| = 15 (calculated when 120 is added, SMA was 105)
// AvgDev = (0+10+15)/3 = 8.333...
// Upper = 110 + 2*8.333 = 126.666..., Lower = 110 - 2*8.333 = 93.333...
// Dev3 = |120 - 110| = 10 (same-bar SMA)
// AvgDev = (0+5+10)/3 = 5.0
// Upper = 110 + 2*5 = 120, Lower = 110 - 2*5 = 100
abber.Update(new TValue(DateTime.UtcNow, 120));
Assert.Equal(110.0, abber.Last.Value, 1e-10);
Assert.Equal(110.0 + 2.0 * 25.0 / 3.0, abber.Upper.Value, 1e-10);
Assert.Equal(110.0 - 2.0 * 25.0 / 3.0, abber.Lower.Value, 1e-10);
Assert.Equal(120.0, abber.Upper.Value, 1e-10);
Assert.Equal(100.0, abber.Lower.Value, 1e-10);
}
[Fact]
+6 -6
View File
@@ -36,11 +36,11 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
[Fact]
public void Validate_ManualCalculation_Period3()
{
// Manual calculation verification
// Manual calculation verification (same-bar SMA deviation)
// Values: [100, 110, 120]
// Bar 1: SMA=100, Dev=0, AvgDev=0
// Bar 2: SMA=(100+110)/2=105, Dev1=0, Dev2=|110-100|=10, AvgDev=(0+10)/2=5
// Bar 3: SMA=(100+110+120)/3=110, Dev3=|120-105|=15, AvgDev=(0+10+15)/3=8.333
// Bar 1: SMA=100, Dev=|100-100|=0, AvgDev=0
// Bar 2: SMA=(100+110)/2=105, Dev2=|110-105|=5, AvgDev=(0+5)/2=2.5
// Bar 3: SMA=(100+110+120)/3=110, Dev3=|120-110|=10, AvgDev=(0+5+10)/3=5.0
var series = new TSeries();
var time = DateTime.UtcNow;
@@ -54,8 +54,8 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
// SMA(3) = 110
Assert.Equal(110.0, middle.Last.Value, 1e-10);
// AvgDev = (0 + 10 + 15) / 3 = 25/3
const double expectedAvgDev = 25.0 / 3.0;
// AvgDev = (0 + 5 + 10) / 3 = 5.0
const double expectedAvgDev = 5.0;
double expectedBandWidth = 2.0 * expectedAvgDev;
Assert.Equal(110.0 + expectedBandWidth, upper.Last.Value, 1e-10);
+27 -19
View File
@@ -190,9 +190,12 @@ public sealed class Abber : ITValuePublisher, IDisposable
{
_pState = _state;
// Calculate SMA first to get deviation
// Compute SMA including the new value with correct divisor (matches batch ProcessMainLoop)
int count = _sourceBuffer.Count;
double sma = count > 0 ? _state.SumSource / count : value;
double removedSource = count == _sourceBuffer.Capacity ? _sourceBuffer.Oldest : 0.0;
double newSum = _state.SumSource - removedSource + value;
int newCount = count < _sourceBuffer.Capacity ? count + 1 : count;
double sma = newSum / newCount;
double deviation = Math.Abs(value - sma);
UpdateState(value, deviation);
@@ -201,17 +204,18 @@ public sealed class Abber : ITValuePublisher, IDisposable
{
_state = _pState;
// Calculate SMA first to get deviation
int count = _sourceBuffer.Count;
double sma = count > 0 ? _state.SumSource / count : value;
double deviation = Math.Abs(value - sma);
// Replace newest source value and recompute sum for current-bar SMA (matches Pine)
_sourceBuffer.UpdateNewest(value);
_deviationBuffer.UpdateNewest(deviation);
double currentSum = _sourceBuffer.Sum;
int corrCount = _sourceBuffer.Count;
double corrSma = corrCount > 0 ? currentSum / corrCount : value;
double corrDeviation = Math.Abs(value - corrSma);
_deviationBuffer.UpdateNewest(corrDeviation);
_state = _state with
{
SumSource = _sourceBuffer.Sum,
SumSource = currentSum,
SumDeviation = _deviationBuffer.Sum,
};
}
@@ -357,9 +361,12 @@ public sealed class Abber : ITValuePublisher, IDisposable
{
double value = GetValidValue(source[i].Value);
// Calculate SMA to get deviation
// Compute SMA including the new value with correct divisor (matches batch ProcessMainLoop)
int count = _sourceBuffer.Count;
double sma = count > 0 ? _state.SumSource / count : value;
double removedSource = count == _sourceBuffer.Capacity ? _sourceBuffer.Oldest : 0.0;
double newSum = _state.SumSource - removedSource + value;
int newCount = count < _sourceBuffer.Capacity ? count + 1 : count;
double sma = newSum / newCount;
double deviation = Math.Abs(value - sma);
UpdateState(value, deviation);
@@ -606,18 +613,18 @@ public sealed class Abber : ITValuePublisher, IDisposable
{
double v = GetValidValue(source, i, ref state);
// Calculate current SMA to get deviation
int count = i;
double sma = count > 0 ? state.SumSource / count : v;
// Compute SMA including the new value to get same-bar deviation (matches Pine)
int newCount = i + 1;
double newSum = state.SumSource + v;
double sma = newSum / newCount;
double deviation = Math.Abs(v - sma);
state.SumSource += v;
state.SumSource = newSum;
state.SumDeviation += deviation;
buffers.Source[i] = v;
buffers.Deviation[i] = deviation;
int newCount = i + 1;
double middle = state.SumSource / newCount;
double avgDeviation = state.SumDeviation / newCount;
WriteBandOutputs(outputs, i, middle, avgDeviation, multiplier);
@@ -639,12 +646,13 @@ public sealed class Abber : ITValuePublisher, IDisposable
{
double v = GetValidValue(source, i, ref state);
// Calculate current SMA to get deviation
double sma = state.SumSource / period;
// Compute SMA including the new value to get same-bar deviation (matches Pine)
double newSumSource = state.SumSource - buffers.Source[state.BufferIndex] + v;
double sma = newSumSource / period;
double deviation = Math.Abs(v - sma);
// Update running sums using single buffer index
state.SumSource = state.SumSource - buffers.Source[state.BufferIndex] + v;
state.SumSource = newSumSource;
buffers.Source[state.BufferIndex] = v;
state.SumDeviation = state.SumDeviation - buffers.Deviation[state.BufferIndex] + deviation;
@@ -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)
+38 -40
View File
@@ -1,56 +1,54 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Andrews' Pitchfork (AP)", "AP", overlay=true)
indicator("Adaptive Price Channel (APCHANNEL)", "APCHANNEL", overlay=true)
//@function Calculates Andrews' Pitchfork lines based on three pivot points
//@param p1_back Bars back to first pivot point (leftmost)
//@param p2_back Bars back to second pivot point (middle)
//@param p3_back Bars back to third pivot point (rightmost)
//@returns tuple of [median, upper, lower] lines for current bar
//@optimized Geometric projection with O(1) complexity per bar
apchannel(simple int p1_back, simple int p2_back, simple int p3_back) =>
if p1_back <= 0 or p2_back <= 0 or p3_back <= 0 or not (p1_back > p2_back and p2_back > p3_back)
runtime.error("Use P1 oldest, P2 newer, P3 newest — all >0")
[na, na, na]
int p1_b = math.min(p1_back, bar_index)
int p2_b = math.min(p2_back, bar_index)
int p3_b = math.min(p3_back, bar_index)
int p1_time = bar_index - p1_b
int p2_time = bar_index - p2_b
int p3_time = bar_index - p3_b
float p1_price = nz(close[p1_b])
float p2_price = nz(high[p2_b])
float p3_price = nz(low[p3_b])
if na(close[p1_b]) or na(high[p2_b]) or na(low[p3_b])
//@function Calculates the Adaptive Price Channel using dual EMA on highs and lows.
//@doc The channel applies exponential smoothing to price highs and lows independently,
// creating a dynamic envelope that "remembers" significant extremes while gradually
// fading their influence. Unlike fixed-window Donchian channels that drop extremes
// abruptly, APCHANNEL decays them smoothly (leaky integration).
//@param alpha Smoothing factor (0 < alpha <= 1). Higher = faster decay, shorter memory.
//@returns tuple of [middle, upper, lower] band values
//@optimized O(1) per bar via EMA recursion; uses FMA pattern: decay*prev + alpha*new
apchannel(simple float alpha) =>
if alpha <= 0.0 or alpha > 1.0
runtime.error("Alpha must be > 0 and <= 1")
[float(na), float(na), float(na)]
float mid_time_float = (float(p2_time) + float(p3_time)) / 2.0
float mid_price = (p2_price + p3_price) / 2.0
float time_diff = mid_time_float - float(p1_time)
float median_slope = math.abs(time_diff) > 1e-10 ? (mid_price - p1_price) / time_diff : 0.0
float median_value = p1_price + median_slope * (float(bar_index) - float(p1_time))
float upper_value = p2_price + median_slope * (float(bar_index) - float(p2_time))
float lower_value = p3_price + median_slope * (float(bar_index) - float(p3_time))
if math.abs(median_value) > 1e9 or math.abs(upper_value) > 1e9 or math.abs(lower_value) > 1e9
[float(na), float(na), float(na)]
[median_value, upper_value, lower_value]
float decay = 1.0 - alpha
// EMA of highs (upper band)
var float high_ema = na
float valid_high = nz(high, nz(high_ema, 0.0))
if na(high_ema)
high_ema := valid_high
else
high_ema := decay * high_ema + alpha * valid_high
// EMA of lows (lower band)
var float low_ema = na
float valid_low = nz(low, nz(low_ema, 0.0))
if na(low_ema)
low_ema := valid_low
else
low_ema := decay * low_ema + alpha * valid_low
// Midpoint
float mid = (high_ema + low_ema) / 2.0
[mid, high_ema, low_ema]
// ---------- Main loop ----------
// Inputs
i_p1_back = input.int(45, "Point 1 (Leftmost)", minval=1)
i_p2_back = input.int(30, "Point 2 (Second)", minval=1)
i_p3_back = input.int(15, "Point 3 (Third)", minval=1)
// Validation
if i_p1_back <= i_p2_back or i_p2_back <= i_p3_back
runtime.error("Points must be in chronological order (P1 > P2 > P3)")
i_alpha = input.float(0.2, "Alpha (smoothing factor)", minval=0.01, maxval=1.0, step=0.01)
// Calculation
[median, upper, lower] = apchannel(i_p1_back, i_p2_back, i_p3_back)
[middle, upper, lower] = apchannel(i_alpha)
// Plot
plot(median, "Median", color=color.yellow, linewidth=2)
plot(middle, "Middle", color=color.yellow, linewidth=2)
p1 = plot(upper, "Upper", color=color.new(color.blue, 50), linewidth=1)
p2 = plot(lower, "Lower", color=color.new(color.blue, 50), linewidth=1)
fill(p1, p2, color=color.new(color.blue, 90))
+20 -4
View File
@@ -295,9 +295,17 @@ public sealed class Decaychannel : ITValuePublisher
double top = Math.Min(decayedMax, rawMax);
double bot = Math.Max(decayedMin, rawMin);
// Guard: aggressive decay can cause bot > top; clamp to midpoint
if (bot > top)
{
double clamp = (top + bot) * 0.5;
top = clamp;
bot = clamp;
}
// Update tracked values for next iteration
_currentMax = Math.Max(top, rawMax);
_currentMin = Math.Min(bot, rawMin);
_currentMax = top;
_currentMin = bot;
double mid = (top + bot) * 0.5;
@@ -477,8 +485,16 @@ public sealed class Decaychannel : ITValuePublisher
double top = Math.Min(decayedMax, rawMax);
double bot = Math.Max(decayedMin, rawMin);
currentMax = Math.Max(top, rawMax);
currentMin = Math.Min(bot, rawMin);
// Guard: aggressive decay can cause bot > top; clamp to midpoint
if (bot > top)
{
double clamp = (top + bot) * 0.5;
top = clamp;
bot = clamp;
}
currentMax = top;
currentMin = bot;
double mid = (top + bot) * 0.5;
+13 -16
View File
@@ -12,7 +12,6 @@ public class JbandsIndicatorTests
Assert.Equal(7, ind.Period);
Assert.Equal(0, ind.Phase);
Assert.Equal(0.45, ind.Power);
Assert.True(ind.ShowColdValues);
Assert.Equal("Jbands - Jurik Adaptive Envelope Bands", ind.Name);
Assert.False(ind.SeparateWindow);
@@ -171,32 +170,30 @@ public class JbandsIndicatorTests
}
[Fact]
public void Power_Parameter_Stored_Correctly()
public void Phase_Parameter_Stored_Correctly()
{
// Power parameter is accepted and stored but not currently used in Jbands calculation.
// This test verifies the parameter is properly stored and accessible.
var indLow = new JbandsIndicator { Period = 7, Power = 0.3 };
var indHigh = new JbandsIndicator { Period = 7, Power = 0.8 };
var indPos = new JbandsIndicator { Period = 7, Phase = 50 };
var indNeg = new JbandsIndicator { Period = 7, Phase = -50 };
Assert.Equal(0.3, indLow.Power);
Assert.Equal(0.8, indHigh.Power);
Assert.Equal(50, indPos.Phase);
Assert.Equal(-50, indNeg.Phase);
// Verify both indicators produce valid output
indLow.Initialize();
indHigh.Initialize();
indPos.Initialize();
indNeg.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 30; i++)
{
double price = 100 + Math.Sin(i * 0.3) * 10;
indLow.HistoricalData.AddBar(now.AddMinutes(i), price - 1, price + 2, price - 2, price);
indHigh.HistoricalData.AddBar(now.AddMinutes(i), price - 1, price + 2, price - 2, price);
indLow.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
indHigh.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
indPos.HistoricalData.AddBar(now.AddMinutes(i), price - 1, price + 2, price - 2, price);
indNeg.HistoricalData.AddBar(now.AddMinutes(i), price - 1, price + 2, price - 2, price);
indPos.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
indNeg.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
}
// Both should produce finite values
Assert.True(double.IsFinite(indLow.LinesSeries[0].GetValue(0)));
Assert.True(double.IsFinite(indHigh.LinesSeries[0].GetValue(0)));
Assert.True(double.IsFinite(indPos.LinesSeries[0].GetValue(0)));
Assert.True(double.IsFinite(indNeg.LinesSeries[0].GetValue(0)));
}
}
+1 -4
View File
@@ -18,9 +18,6 @@ public sealed class JbandsIndicator : Indicator, IWatchlistIndicator
[InputParameter("Phase", sortIndex: 20, minimum: -100, maximum: 100, increment: 1, decimalPlaces: 0)]
public int Phase { get; set; } = 0;
[InputParameter("Power", sortIndex: 30, minimum: 0.01, maximum: 5.0, increment: 0.01, decimalPlaces: 2)]
public double Power { get; set; } = 0.45;
[InputParameter("Show Cold Values", sortIndex: 100)]
public bool ShowColdValues { get; set; } = true;
@@ -39,7 +36,7 @@ public sealed class JbandsIndicator : Indicator, IWatchlistIndicator
protected override void OnInit()
{
_indicator = new Jbands(Period, Phase, Power);
_indicator = new Jbands(Period, Phase);
AddLineSeries(new LineSeries("Middle", Color.DodgerBlue, 2, LineStyle.Solid));
AddLineSeries(new LineSeries("Upper", Color.FromArgb(255, 180, 180), 1, LineStyle.Dash));
+11 -11
View File
@@ -11,7 +11,7 @@ public class JbandsTests
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Jbands(0));
Assert.Throws<ArgumentOutOfRangeException>(() => new Jbands(-5));
Assert.Throws<ArgumentException>(() => new Jbands(14, 0, double.NaN));
// power parameter removed - no longer applicable
var j = new Jbands(14);
Assert.Contains("Jbands", j.Name, StringComparison.OrdinalIgnoreCase);
@@ -19,10 +19,10 @@ public class JbandsTests
}
[Fact]
public void Jbands_Constructor_InfinityPower_Throws()
public void Jbands_Constructor_Period1_IsValid()
{
Assert.Throws<ArgumentException>(() => new Jbands(14, 0, double.PositiveInfinity));
Assert.Throws<ArgumentException>(() => new Jbands(14, 0, double.NegativeInfinity));
var j = new Jbands(1);
Assert.True(j.WarmupPeriod > 0);
}
[Fact]
@@ -276,7 +276,7 @@ public class JbandsTests
[Fact]
public void Jbands_Reset_ThenReuse_ProducesSameResults()
{
var j = new Jbands(14, 0, 0.45);
var j = new Jbands(14, 0);
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 88);
double[] prices = new double[100];
for (int i = 0; i < prices.Length; i++)
@@ -424,8 +424,8 @@ public class JbandsTests
[Fact]
public void Jbands_Prime_MatchesStreamingResults()
{
var jPrime = new Jbands(14, 0, 0.45);
var jStream = new Jbands(14, 0, 0.45);
var jPrime = new Jbands(14, 0);
var jStream = new Jbands(14, 0);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
var series = new TSeries();
@@ -459,7 +459,7 @@ public class JbandsTests
series.Add(bar.Time, bar.Close);
}
var (results, indicator) = Jbands.Calculate(series, 14, 0, 0.45);
var (results, indicator) = Jbands.Calculate(series, 14, 0);
Assert.True(indicator.IsHot);
Assert.Equal(300, results.Middle.Count);
@@ -513,7 +513,7 @@ public class JbandsTests
[Fact]
public void Jbands_BatchVsStreaming_Match()
{
var jStream = new Jbands(14, 0, 0.45);
var jStream = new Jbands(14, 0);
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 42);
var series = new TSeries();
@@ -528,7 +528,7 @@ public class JbandsTests
double expectedUp = jStream.Upper.Value;
double expectedLo = jStream.Lower.Value;
var (midBatch, upBatch, loBatch) = Jbands.Batch(series, 14, 0, 0.45);
var (midBatch, upBatch, loBatch) = Jbands.Batch(series, 14, 0);
Assert.Equal(expectedMid, midBatch.Last.Value, 1e-10);
Assert.Equal(expectedUp, upBatch.Last.Value, 1e-10);
@@ -634,7 +634,7 @@ public class JbandsTests
public void Jbands_MiddleBand_MatchesJma()
{
// Verify that middle band matches standalone JMA
var jbands = new Jbands(14, 0, 0.45);
var jbands = new Jbands(14, 0);
var jma = new Jma(14, 0, 0.45);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 999);
+17 -17
View File
@@ -16,34 +16,34 @@ public class JbandsValidationTests
[Fact]
public void Jbands_MiddleBand_MatchesJma_Period7()
{
ValidateMiddleBandMatchesJma(7, 0, 0.45, 42);
ValidateMiddleBandMatchesJma(7, 0, 42);
}
[Fact]
public void Jbands_MiddleBand_MatchesJma_Period14()
{
ValidateMiddleBandMatchesJma(14, 0, 0.45, 123);
ValidateMiddleBandMatchesJma(14, 0, 123);
}
[Fact]
public void Jbands_MiddleBand_MatchesJma_Period20()
{
ValidateMiddleBandMatchesJma(20, 0, 0.45, 456);
ValidateMiddleBandMatchesJma(20, 0, 456);
}
[Fact]
public void Jbands_MiddleBand_MatchesJma_WithPhase()
{
ValidateMiddleBandMatchesJma(14, 50, 0.45, 789);
ValidateMiddleBandMatchesJma(14, -50, 0.45, 321);
ValidateMiddleBandMatchesJma(14, 100, 0.45, 654);
ValidateMiddleBandMatchesJma(14, -100, 0.45, 987);
ValidateMiddleBandMatchesJma(14, 50, 789);
ValidateMiddleBandMatchesJma(14, -50, 321);
ValidateMiddleBandMatchesJma(14, 100, 654);
ValidateMiddleBandMatchesJma(14, -100, 987);
}
private static void ValidateMiddleBandMatchesJma(int period, int phase, double power, int seed)
private static void ValidateMiddleBandMatchesJma(int period, int phase, int seed)
{
var jbands = new Jbands(period, phase, power);
var jma = new Jma(period, phase, power);
var jbands = new Jbands(period, phase);
var jma = new Jma(period, phase);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: seed);
for (int i = 0; i < 500; i++)
@@ -60,7 +60,7 @@ public class JbandsValidationTests
[Fact]
public void Jbands_StreamingVsBatch_Match()
{
var jStream = new Jbands(14, 0, 0.45);
var jStream = new Jbands(14, 0);
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 42);
var series = new TSeries();
@@ -71,13 +71,13 @@ public class JbandsValidationTests
jStream.Update(new TValue(bar.Time, bar.Close), isNew: true);
}
var (midBatch, upBatch, loBatch) = Jbands.Batch(series, 14, 0, 0.45);
var (midBatch, upBatch, loBatch) = Jbands.Batch(series, 14, 0);
// Compare last 100 values
for (int i = series.Count - 100; i < series.Count; i++)
{
// Rebuild streaming to get value at index i
var jCheck = new Jbands(14, 0, 0.45);
var jCheck = new Jbands(14, 0);
for (int j = 0; j <= i; j++)
{
jCheck.Update(new TValue(new DateTime(series.Times[j], DateTimeKind.Utc), series.Values[j]), isNew: true);
@@ -131,23 +131,23 @@ public class JbandsValidationTests
}
// Mode 1: Streaming
var jStream = new Jbands(14, 25, 0.45);
var jStream = new Jbands(14, 25);
for (int i = 0; i < rawValues.Length; i++)
{
jStream.Update(new TValue(DateTime.UtcNow, rawValues[i]), isNew: true);
}
// Mode 2: Batch (TSeries)
var (midBatch, upBatch, loBatch) = Jbands.Batch(series, 14, 25, 0.45);
var (midBatch, upBatch, loBatch) = Jbands.Batch(series, 14, 25);
// Mode 3: Span Calculate
double[] middleSpan = new double[150];
double[] upperSpan = new double[150];
double[] lowerSpan = new double[150];
Jbands.Batch(rawValues.AsSpan(), middleSpan.AsSpan(), upperSpan.AsSpan(), lowerSpan.AsSpan(), 14, 25, 0.45);
Jbands.Batch(rawValues.AsSpan(), middleSpan.AsSpan(), upperSpan.AsSpan(), lowerSpan.AsSpan(), 14, 25);
// Mode 4: Event-based
var jEvent = new Jbands(14, 25, 0.45);
var jEvent = new Jbands(14, 25);
double lastEventMid = 0, lastEventUp = 0, lastEventLo = 0;
jEvent.Pub += (object? sender, in TValueEventArgs args) =>
{
+10 -16
View File
@@ -60,18 +60,13 @@ public sealed class Jbands : ITValuePublisher, IDisposable
public event TValuePublishedHandler? Pub;
public Jbands(int period, int phase = 0, double power = 0.45)
public Jbands(int period, int phase = 0)
{
if (period < 1)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1.");
}
if (!double.IsFinite(power))
{
throw new ArgumentException("Power must be finite.", nameof(power));
}
// Phase parameter: maps -100..100 -> 0.5..2.5
if (phase < -100)
{
@@ -107,7 +102,7 @@ public sealed class Jbands : ITValuePublisher, IDisposable
WarmupPeriod = (int)Math.Ceiling(20.0 + 80.0 * Math.Pow(period, 0.36));
_handler = Handle;
Name = $"Jbands({period},{phase},{power})";
Name = $"Jbands({period},{phase})";
_devBuffer = new RingBuffer(DevWindowSize);
_volBuffer = new RingBuffer(VolWindowSize);
@@ -115,8 +110,8 @@ public sealed class Jbands : ITValuePublisher, IDisposable
Reset();
}
public Jbands(ITValuePublisher source, int period, int phase = 0, double power = 0.45)
: this(period, phase, power)
public Jbands(ITValuePublisher source, int period, int phase = 0)
: this(period, phase)
{
_source = source;
source.Pub += _handler;
@@ -380,9 +375,9 @@ public sealed class Jbands : ITValuePublisher, IDisposable
}
}
public static (TSeries Middle, TSeries Upper, TSeries Lower) Batch(TSeries source, int period, int phase = 0, double power = 0.45)
public static (TSeries Middle, TSeries Upper, TSeries Lower) Batch(TSeries source, int period, int phase = 0)
{
var jbands = new Jbands(period, phase, power);
var jbands = new Jbands(period, phase);
return jbands.Update(source);
}
@@ -392,8 +387,7 @@ public sealed class Jbands : ITValuePublisher, IDisposable
Span<double> upper,
Span<double> lower,
int period,
int phase = 0,
double power = 0.45)
int phase = 0)
{
if (middle.Length != source.Length)
{
@@ -415,7 +409,7 @@ public sealed class Jbands : ITValuePublisher, IDisposable
return;
}
var jbands = new Jbands(period, phase, power);
var jbands = new Jbands(period, phase);
for (int i = 0; i < source.Length; i++)
{
var (jma, u, l) = jbands.Step(source[i], isNew: true);
@@ -428,9 +422,9 @@ public sealed class Jbands : ITValuePublisher, IDisposable
/// <summary>
/// Calculates Jbands and returns both the results and the indicator instance.
/// </summary>
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Jbands Indicator) Calculate(TSeries source, int period, int phase = 0, double power = 0.45)
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Jbands Indicator) Calculate(TSeries source, int period, int phase = 0)
{
var indicator = new Jbands(period, phase, power);
var indicator = new Jbands(period, phase);
var results = indicator.Update(source);
return (results, indicator);
}
@@ -14,9 +14,12 @@ namespace QuanTAlib;
/// </summary>
public sealed class StarchannelIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 10, minimum: 1, maximum: 500, increment: 1, decimalPlaces: 0)]
[InputParameter("SMA Period", sortIndex: 10, minimum: 1, maximum: 500, increment: 1, decimalPlaces: 0)]
public int Period { get; set; } = 20;
[InputParameter("ATR Period (0 = same as SMA)", sortIndex: 15, minimum: 0, maximum: 500, increment: 1, decimalPlaces: 0)]
public int AtrPeriod { get; set; } = 0;
[InputParameter("Multiplier", sortIndex: 20, minimum: 0.1, maximum: 10.0, increment: 0.1, decimalPlaces: 1)]
public double Multiplier { get; set; } = 2.0;
@@ -25,8 +28,10 @@ public sealed class StarchannelIndicator : Indicator, IWatchlistIndicator
private Starchannel? _indicator;
public int MinHistoryDepths => Period;
public override string ShortName => $"Starchannel({Period},{Multiplier})";
public int MinHistoryDepths => Math.Max(Period, AtrPeriod > 0 ? AtrPeriod : Period);
public override string ShortName => AtrPeriod > 0 && AtrPeriod != Period
? $"Starchannel({Period},{Multiplier},{AtrPeriod})"
: $"Starchannel({Period},{Multiplier})";
public StarchannelIndicator()
{
@@ -38,7 +43,7 @@ public sealed class StarchannelIndicator : Indicator, IWatchlistIndicator
protected override void OnInit()
{
_indicator = new Starchannel(Period, Multiplier);
_indicator = new Starchannel(Period, Multiplier, AtrPeriod);
AddLineSeries(new LineSeries("Middle", Color.DodgerBlue, 2, LineStyle.Solid));
AddLineSeries(new LineSeries("Upper", Color.FromArgb(255, 180, 180), 1, LineStyle.Dash));
+34 -18
View File
@@ -7,14 +7,16 @@ namespace QuanTAlib;
/// STARCHANNEL: Stoller Average Range Channel
/// A volatility-based envelope using SMA as the middle line and ATR for band width.
/// Middle = SMA(source, period)
/// Upper = Middle + (multiplier × ATR)
/// Lower = Middle - (multiplier × ATR)
/// Upper = Middle + (multiplier × ATR(atrPeriod))
/// Lower = Middle - (multiplier × ATR(atrPeriod))
/// ATR uses RMA (Wilder's smoothing) with warmup compensation.
/// Supports separate SMA and ATR periods for traditional Stoller dual-period design.
/// </summary>
[SkipLocalsInit]
public sealed class Starchannel : ITValuePublisher
{
private readonly int _period;
private readonly int _atrPeriod;
private readonly double _multiplier;
private readonly double _atrAlpha;
private readonly RingBuffer _smaBuffer;
@@ -46,7 +48,7 @@ public sealed class Starchannel : ITValuePublisher
public event TValuePublishedHandler? Pub;
public Starchannel(int period = 20, double multiplier = 2.0)
public Starchannel(int period = 20, double multiplier = 2.0, int atrPeriod = 0)
{
if (period < 1)
{
@@ -58,20 +60,30 @@ public sealed class Starchannel : ITValuePublisher
throw new ArgumentOutOfRangeException(nameof(multiplier), "Multiplier must be > 0.");
}
// Default atrPeriod to period when 0 (backward compatible)
int effectiveAtrPeriod = atrPeriod > 0 ? atrPeriod : period;
if (effectiveAtrPeriod < 1)
{
throw new ArgumentOutOfRangeException(nameof(atrPeriod), "ATR period must be >= 1.");
}
_period = period;
_atrPeriod = effectiveAtrPeriod;
_multiplier = multiplier;
_atrAlpha = 1.0 / period;
_atrAlpha = 1.0 / effectiveAtrPeriod;
_smaBuffer = new RingBuffer(period);
WarmupPeriod = period;
WarmupPeriod = Math.Max(period, effectiveAtrPeriod);
Name = $"Starchannel({period},{multiplier})";
Name = effectiveAtrPeriod == period
? $"Starchannel({period},{multiplier})"
: $"Starchannel({period},{multiplier},{effectiveAtrPeriod})";
_barHandler = HandleBar;
Reset();
}
public Starchannel(TBarSeries source, int period = 20, double multiplier = 2.0) : this(period, multiplier)
public Starchannel(TBarSeries source, int period = 20, double multiplier = 2.0, int atrPeriod = 0) : this(period, multiplier, atrPeriod)
{
Prime(source);
source.Pub += _barHandler;
@@ -179,8 +191,8 @@ public sealed class Starchannel : ITValuePublisher
double tr3 = Math.Abs(low - prevClose);
double trueRange = Math.Max(tr1, Math.Max(tr2, tr3));
// ATR using RMA with warmup compensation
double newRawRma = (_state.RawRma * (_period - 1) + trueRange) / _period;
// ATR using RMA with warmup compensation (uses _atrPeriod for separate ATR smoothing)
double newRawRma = (_state.RawRma * (_atrPeriod - 1) + trueRange) / _atrPeriod;
double newE = (1.0 - _atrAlpha) * _state.E;
double atrValue = newE > Epsilon ? newRawRma / (1.0 - newE) : newRawRma;
@@ -238,7 +250,7 @@ public sealed class Starchannel : ITValuePublisher
var vLowerSpan = CollectionsMarshal.AsSpan(vLower);
Batch(source.HighValues, source.LowValues, source.CloseValues,
vMiddleSpan, vUpperSpan, vLowerSpan, _period, _multiplier);
vMiddleSpan, vUpperSpan, vLowerSpan, _period, _multiplier, _atrPeriod);
source.Times.CopyTo(tSpan);
tSpan.CopyTo(CollectionsMarshal.AsSpan(tUpper));
@@ -281,7 +293,8 @@ public sealed class Starchannel : ITValuePublisher
Span<double> upper,
Span<double> lower,
int period,
double multiplier = 2.0)
double multiplier = 2.0,
int atrPeriod = 0)
{
if (period < 1)
{
@@ -293,6 +306,9 @@ public sealed class Starchannel : ITValuePublisher
throw new ArgumentOutOfRangeException(nameof(multiplier), "Multiplier must be > 0.");
}
// Default atrPeriod to period when 0 (backward compatible)
int effectiveAtrPeriod = atrPeriod > 0 ? atrPeriod : period;
if (high.Length != low.Length || high.Length != close.Length)
{
throw new ArgumentException("High, Low, and Close spans must have the same length", nameof(high));
@@ -309,7 +325,7 @@ public sealed class Starchannel : ITValuePublisher
return;
}
double atrAlpha = 1.0 / period;
double atrAlpha = 1.0 / effectiveAtrPeriod;
// First bar - sanitize first values
double lastValidClose = double.IsFinite(close[0]) ? close[0] : 0;
@@ -389,8 +405,8 @@ public sealed class Starchannel : ITValuePublisher
double tr3 = Math.Abs(l - prevClose);
double tr = Math.Max(tr1, Math.Max(tr2, tr3));
// ATR (RMA with warmup compensation)
rawRma = (rawRma * (period - 1) + tr) / period;
// ATR (RMA with warmup compensation, uses effectiveAtrPeriod)
rawRma = (rawRma * (effectiveAtrPeriod - 1) + tr) / effectiveAtrPeriod;
e = (1.0 - atrAlpha) * e;
double atr = e > Epsilon ? rawRma / (1.0 - e) : rawRma;
@@ -403,7 +419,7 @@ public sealed class Starchannel : ITValuePublisher
}
}
public static (TSeries Middle, TSeries Upper, TSeries Lower) Batch(TBarSeries source, int period = 20, double multiplier = 2.0)
public static (TSeries Middle, TSeries Upper, TSeries Lower) Batch(TBarSeries source, int period = 20, double multiplier = 2.0, int atrPeriod = 0)
{
int len = source.Count;
var tMiddle = new List<long>(len);
@@ -424,7 +440,7 @@ public sealed class Starchannel : ITValuePublisher
CollectionsMarshal.AsSpan(vMiddle),
CollectionsMarshal.AsSpan(vUpper),
CollectionsMarshal.AsSpan(vLower),
period, multiplier);
period, multiplier, atrPeriod);
source.Times.CopyTo(CollectionsMarshal.AsSpan(tMiddle));
CollectionsMarshal.AsSpan(tMiddle).CopyTo(CollectionsMarshal.AsSpan(tUpper));
@@ -433,9 +449,9 @@ public sealed class Starchannel : ITValuePublisher
return (new TSeries(tMiddle, vMiddle), new TSeries(tUpper, vUpper), new TSeries(tLower, vLower));
}
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Starchannel Indicator) Calculate(TBarSeries source, int period = 20, double multiplier = 2.0)
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Starchannel Indicator) Calculate(TBarSeries source, int period = 20, double multiplier = 2.0, int atrPeriod = 0)
{
var indicator = new Starchannel(source, period, multiplier);
var indicator = new Starchannel(source, period, multiplier, atrPeriod);
var results = indicator.Update(source);
return (results, indicator);
}
+9 -6
View File
@@ -5,13 +5,15 @@ indicator("Stoller Average Range Channel (STARCHANNEL)", "STARCHANNEL", overlay=
//@function Calculates Stoller Average Range Channel using ATR for width and SMA for center
//@param source Source series for the center line
//@param length Period for ATR and SMA calculations
//@param length Period for SMA calculation
//@param multiplier ATR multiplier for band width
//@param atr_length Period for ATR calculation (0 = same as length)
//@returns tuple with [middle, upper, lower] band values
//@optimized Uses circular buffer for SMA and ATR with compensator, O(1) complexity
starchannel(series float source, simple int length, simple float multiplier) =>
starchannel(series float source, simple int length, simple float multiplier, simple int atr_length = 0) =>
if length <= 0 or multiplier <= 0.0
runtime.error("Length and multiplier must be greater than 0")
int effective_atr_length = atr_length > 0 ? atr_length : length
var float prevClose = close
float tr1 = high - low
float tr2 = math.abs(high - prevClose)
@@ -44,8 +46,8 @@ starchannel(series float source, simple int length, simple float multiplier) =>
var float e = 1.0
float atrValue = na
if not na(trueRange)
float alpha = 1.0 / float(length)
raw_rma := (raw_rma * (length - 1) + trueRange) / length
float alpha = 1.0 / float(effective_atr_length)
raw_rma := (raw_rma * (effective_atr_length - 1) + trueRange) / effective_atr_length
e := (1.0 - alpha) * e
atrValue := e > EPSILON ? raw_rma / (1.0 - e) : raw_rma
float middleBand = nz(sumSource / count, source)
@@ -56,11 +58,12 @@ starchannel(series float source, simple int length, simple float multiplier) =>
// Inputs
i_source = input.source(close, "Source")
i_length = input.int(20, "Length", minval=1)
i_length = input.int(20, "SMA Length", minval=1)
i_atr_length = input.int(0, "ATR Length (0 = same as SMA)", minval=0)
i_mult = input.float(2.0, "ATR Multiplier", minval=0.001)
// Calculation
[middle, upper, lower] = starchannel(i_source, i_length, i_mult)
[middle, upper, lower] = starchannel(i_source, i_length, i_mult, i_atr_length)
// Plot
plot(middle, "Middle", color=color.yellow, linewidth=2)
@@ -180,12 +180,11 @@ public class TtmLrcIndicatorTests
var ind = new TtmLrcIndicator { Period = 10 };
ind.Initialize();
var now = DateTime.UtcNow;
var rng = new Random(42);
var bars = new GBM(seed: 42).Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 20; i++)
{
double price = 100 + rng.NextDouble() * 20;
ind.HistoricalData.AddBar(now.AddMinutes(i), price, price + 5, price - 5, price);
var bar = bars[i];
ind.HistoricalData.AddBar(bar.AsDateTime, bar.Open, bar.High, bar.Low, bar.Close);
ind.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
}
@@ -285,11 +284,11 @@ public class TtmLrcIndicatorTests
ind.Initialize();
var now = DateTime.UtcNow;
var rng = new Random(42);
var bars = new GBM(seed: 42).Fetch(20, now.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 20; i++)
{
double price = 100 + rng.NextDouble() * 30;
ind.HistoricalData.AddBar(now.AddMinutes(i), price, price + 5, price - 5, price);
var bar = bars[i];
ind.HistoricalData.AddBar(bar.AsDateTime, bar.Open, bar.High, bar.Low, bar.Close);
ind.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
}
+19 -53
View File
@@ -96,12 +96,11 @@ public class TtmLrcTests
public void Bands_Symmetry_Upper1AndLower1EquidistantFromMiddle()
{
var indicator = new TtmLrc(10);
var now = DateTime.UtcNow;
var rng = new Random(42);
var bars = new GBM(seed: 42).Fetch(15, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 15; i++)
{
indicator.Update(new TValue(now.AddMinutes(i), 100 + rng.NextDouble() * 10), isNew: true);
indicator.Update(bars.Close[i], isNew: true);
}
double mid = indicator.Midline.Value;
@@ -119,12 +118,11 @@ public class TtmLrcTests
public void Bands_Symmetry_Upper2AndLower2EquidistantFromMiddle()
{
var indicator = new TtmLrc(10);
var now = DateTime.UtcNow;
var rng = new Random(42);
var bars = new GBM(seed: 42).Fetch(15, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 15; i++)
{
indicator.Update(new TValue(now.AddMinutes(i), 100 + rng.NextDouble() * 10), isNew: true);
indicator.Update(bars.Close[i], isNew: true);
}
double mid = indicator.Midline.Value;
@@ -142,12 +140,11 @@ public class TtmLrcTests
public void Bands_Ordering_UpperGreaterThanMiddleGreaterThanLower()
{
var indicator = new TtmLrc(10);
var now = DateTime.UtcNow;
var rng = new Random(42);
var bars = new GBM(seed: 42).Fetch(15, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 15; i++)
{
indicator.Update(new TValue(now.AddMinutes(i), 100 + rng.NextDouble() * 10), isNew: true);
indicator.Update(bars.Close[i], isNew: true);
}
Assert.True(indicator.Upper2.Value >= indicator.Upper1.Value, "Upper2 should be >= Upper1");
@@ -249,12 +246,11 @@ public class TtmLrcTests
public void RSquared_RandomData_LessThanOne()
{
var indicator = new TtmLrc(20);
var now = DateTime.UtcNow;
var rng = new Random(42);
var bars = new GBM(seed: 42).Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 30; i++)
{
indicator.Update(new TValue(now.AddMinutes(i), 100 + rng.NextDouble() * 50), isNew: true);
indicator.Update(bars.Close[i], isNew: true);
}
Assert.True(indicator.RSquared < 1.0, $"R² should be less than 1.0 for random data, got {indicator.RSquared}");
@@ -265,12 +261,11 @@ public class TtmLrcTests
public void RSquared_ClampedBetweenZeroAndOne()
{
var indicator = new TtmLrc(5);
var now = DateTime.UtcNow;
var rng = new Random(123);
var bars = new GBM(seed: 123).Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 20; i++)
{
indicator.Update(new TValue(now.AddMinutes(i), 100 + rng.NextDouble() * 100 - 50), isNew: true);
indicator.Update(bars.Close[i], isNew: true);
Assert.True(indicator.RSquared >= 0.0 && indicator.RSquared <= 1.0, $"R² should be in [0,1], got {indicator.RSquared}");
}
}
@@ -368,23 +363,15 @@ public class TtmLrcTests
public void BatchVsStreaming_SameResults()
{
var streamingIndicator = new TtmLrc(20);
var now = DateTime.UtcNow;
var rng = new Random(42);
int count = 50;
var times = new List<long>(count);
var values = new List<double>(count);
var bars = new GBM(seed: 42).Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < count; i++)
{
long t = (now.AddMinutes(i)).Ticks;
double v = 100 + rng.NextDouble() * 20;
times.Add(t);
values.Add(v);
streamingIndicator.Update(new TValue(new DateTime(t, DateTimeKind.Utc), v), isNew: true);
streamingIndicator.Update(bars.Close[i], isNew: true);
}
var source = new TSeries(times, values);
var source = bars.Close;
var (bMid, bU1, bL1, bU2, bL2) = TtmLrc.Batch(source, 20);
// Compare streaming final values to batch final values
@@ -399,20 +386,10 @@ public class TtmLrcTests
public void Update_TSeries_ReturnsAllFiveBands()
{
var indicator = new TtmLrc(10);
var now = DateTime.UtcNow;
var rng = new Random(42);
int count = 20;
var bars = new GBM(seed: 42).Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var times = new List<long>(count);
var values = new List<double>(count);
for (int i = 0; i < count; i++)
{
times.Add(now.AddMinutes(i).Ticks);
values.Add(100 + rng.NextDouble() * 10);
}
var source = new TSeries(times, values);
var source = bars.Close;
var (mid, u1, l1, u2, l2) = indicator.Update(source);
Assert.Equal(count, mid.Count);
@@ -425,20 +402,10 @@ public class TtmLrcTests
[Fact]
public void Calculate_ReturnsIndicatorAndResults()
{
var now = DateTime.UtcNow;
var rng = new Random(42);
int count = 30;
var bars = new GBM(seed: 42).Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var times = new List<long>(count);
var values = new List<double>(count);
for (int i = 0; i < count; i++)
{
times.Add(now.AddMinutes(i).Ticks);
values.Add(100 + rng.NextDouble() * 15);
}
var source = new TSeries(times, values);
var source = bars.Close;
var (results, indicator) = TtmLrc.Calculate(source, 15);
Assert.NotNull(indicator);
@@ -674,12 +641,11 @@ public class TtmLrcTests
public void LargePeriod_HandlesCorrectly()
{
var indicator = new TtmLrc(200);
var now = DateTime.UtcNow;
var rng = new Random(42);
var bars = new GBM(seed: 42).Fetch(250, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 250; i++)
{
indicator.Update(new TValue(now.AddMinutes(i), 100 + rng.NextDouble() * 50), isNew: true);
indicator.Update(bars.Close[i], isNew: true);
}
Assert.True(indicator.IsHot);
+4 -4
View File
@@ -114,10 +114,10 @@ public sealed class Ubands : AbstractBase
_state = default;
_p_state = default;
_residualBuffer.Clear();
Upper = default;
Middle = default;
Lower = default;
Width = default;
Upper = new TValue(DateTime.UtcNow, double.NaN);
Middle = new TValue(DateTime.UtcNow, double.NaN);
Lower = new TValue(DateTime.UtcNow, double.NaN);
Width = new TValue(DateTime.UtcNow, double.NaN);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]