Refactor documentation for various filters and indicators to enhance clarity and consistency

- Updated Bessel, Bilateral, Blma, Butter, Conv, Ema, Kama, LSMA, MAMA, MGDI, SSF, USF, ATR, ADL, and ADOSC documentation to use bullet points for key concepts and features.
- Added a new Qodana configuration file for code analysis.
- Removed coverage configuration from Quantower.Tests.csproj to streamline testing setup.
This commit is contained in:
Miha Kralj
2025-12-31 23:39:47 -08:00
parent 11f4ec2497
commit d493bfd42f
175 changed files with 11977 additions and 897 deletions
+733
View File
@@ -0,0 +1,733 @@
namespace QuanTAlib.Tests;
public class AccBandsTests
{
[Fact]
public void AccBands_Constructor_ValidatesInput()
{
// Period validation
Assert.Throws<ArgumentException>(() => new AccBands(0));
Assert.Throws<ArgumentException>(() => new AccBands(-1));
// Factor validation
Assert.Throws<ArgumentException>(() => new AccBands(10, 0));
Assert.Throws<ArgumentException>(() => new AccBands(10, -1));
// Valid construction
var accBands = new AccBands(10);
Assert.NotNull(accBands);
var accBands2 = new AccBands(20, 3.0);
Assert.NotNull(accBands2);
}
[Fact]
public void AccBands_Calc_ReturnsValue()
{
var accBands = new AccBands(10);
Assert.Equal(0, accBands.Last.Value);
Assert.Equal(0, accBands.Upper.Value);
Assert.Equal(0, accBands.Lower.Value);
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
TValue result = accBands.Update(bar);
Assert.True(double.IsFinite(result.Value));
Assert.Equal(result.Value, accBands.Last.Value);
Assert.True(double.IsFinite(accBands.Upper.Value));
Assert.True(double.IsFinite(accBands.Lower.Value));
}
[Fact]
public void AccBands_FirstValue_ReturnsExpected()
{
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
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);
}
[Fact]
public void AccBands_Calc_IsNew_AcceptsParameter()
{
var accBands = new AccBands(10);
var bar1 = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
accBands.Update(bar1, isNew: true);
double value1 = accBands.Last.Value;
var bar2 = new TBar(DateTime.UtcNow, 102, 110, 98, 108, 1100);
accBands.Update(bar2, isNew: true);
double value2 = accBands.Last.Value;
// Values should change with new bars
Assert.NotEqual(value1, value2);
}
[Fact]
public void AccBands_Calc_IsNew_False_UpdatesValue()
{
var accBands = new AccBands(10);
var bar1 = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
accBands.Update(bar1, isNew: true);
var bar2 = new TBar(DateTime.UtcNow, 102, 110, 98, 108, 1100);
accBands.Update(bar2, isNew: true);
double beforeUpdate = accBands.Last.Value;
var bar3 = new TBar(DateTime.UtcNow, 102, 112, 100, 111, 1200);
accBands.Update(bar3, isNew: false);
double afterUpdate = accBands.Last.Value;
// Update should change the value
Assert.NotEqual(beforeUpdate, afterUpdate);
}
[Fact]
public void AccBands_Reset_ClearsState()
{
var accBands = new AccBands(10);
var bar1 = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
accBands.Update(bar1);
var bar2 = new TBar(DateTime.UtcNow, 102, 110, 98, 108, 1100);
accBands.Update(bar2);
double middleBefore = accBands.Last.Value;
accBands.Reset();
Assert.Equal(0, accBands.Last.Value);
Assert.Equal(0, accBands.Upper.Value);
Assert.Equal(0, accBands.Lower.Value);
Assert.False(accBands.IsHot);
// After reset, should accept new values
var bar3 = new TBar(DateTime.UtcNow, 50, 55, 45, 52, 500);
accBands.Update(bar3);
Assert.NotEqual(0, accBands.Last.Value);
Assert.NotEqual(middleBefore, accBands.Last.Value);
}
[Fact]
public void AccBands_Properties_Accessible()
{
var accBands = new AccBands(10, 2.5);
Assert.Equal(0, accBands.Last.Value);
Assert.False(accBands.IsHot);
Assert.Contains("AccBands", accBands.Name, StringComparison.Ordinal);
Assert.Equal(10, accBands.WarmupPeriod);
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
accBands.Update(bar);
Assert.NotEqual(0, accBands.Last.Value);
}
[Fact]
public void AccBands_IsHot_BecomesTrueWhenBufferFull()
{
var accBands = new AccBands(5);
Assert.False(accBands.IsHot);
for (int i = 1; i <= 4; i++)
{
var bar = new TBar(DateTime.UtcNow, 100 + i, 105 + i, 95 + i, 102 + i, 1000);
accBands.Update(bar);
Assert.False(accBands.IsHot);
}
var lastBar = new TBar(DateTime.UtcNow, 105, 110, 100, 107, 1000);
accBands.Update(lastBar);
Assert.True(accBands.IsHot);
}
[Fact]
public void AccBands_CalculatesCorrectBands()
{
var accBands = new AccBands(3, 2.0);
// Bar 1: H=110, L=90, C=100
accBands.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000));
// Bar 2: H=115, L=95, C=105
accBands.Update(new TBar(DateTime.UtcNow, 105, 115, 95, 105, 1000));
// Bar 3: H=120, L=100, C=110
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
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);
}
[Fact]
public void AccBands_SlidingWindow_Works()
{
var accBands = new AccBands(3, 2.0);
// Bar 1: H=110, L=90, C=100
accBands.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000));
// Bar 2: H=115, L=95, C=105
accBands.Update(new TBar(DateTime.UtcNow, 105, 115, 95, 105, 1000));
// Bar 3: H=120, L=100, C=110
accBands.Update(new TBar(DateTime.UtcNow, 110, 120, 100, 110, 1000));
double middle1 = accBands.Last.Value;
// Bar 4: H=125, L=105, C=115 - Window slides: [115, 120, 125], [95, 100, 105], [105, 110, 115]
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);
}
[Fact]
public void AccBands_IterativeCorrections_RestoreToOriginalState()
{
var accBands = new AccBands(5);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
// Feed 10 new bars
TBar tenthInput = default;
for (int i = 0; i < 10; i++)
{
var bar = gbm.Next(isNew: true);
tenthInput = bar;
accBands.Update(bar, isNew: true);
}
// Remember state after 10 bars
double middleAfterTen = accBands.Last.Value;
double upperAfterTen = accBands.Upper.Value;
double lowerAfterTen = accBands.Lower.Value;
// Generate 9 corrections with isNew=false (different values)
for (int i = 0; i < 9; i++)
{
var bar = gbm.Next(isNew: false);
accBands.Update(bar, isNew: false);
}
// Feed the remembered 10th input again with isNew=false
accBands.Update(tenthInput, isNew: false);
// State should match the original state after 10 bars
Assert.Equal(middleAfterTen, accBands.Last.Value, 1e-10);
Assert.Equal(upperAfterTen, accBands.Upper.Value, 1e-10);
Assert.Equal(lowerAfterTen, accBands.Lower.Value, 1e-10);
}
[Fact]
public void AccBands_BatchCalc_MatchesIterativeCalc()
{
var accBandsIterative = new AccBands(10);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
// Generate data
var series = new TBarSeries();
for (int i = 0; i < 100; i++)
{
var bar = gbm.Next(isNew: true);
series.Add(bar);
}
Assert.True(series.Count > 0);
// Calculate iteratively
var iterativeMiddle = new List<double>();
var iterativeUpper = new List<double>();
var iterativeLower = new List<double>();
foreach (var bar in series)
{
accBandsIterative.Update(bar);
iterativeMiddle.Add(accBandsIterative.Last.Value);
iterativeUpper.Add(accBandsIterative.Upper.Value);
iterativeLower.Add(accBandsIterative.Lower.Value);
}
// Calculate batch
var accBandsBatch = new AccBands(10);
var (batchMiddle, batchUpper, batchLower) = accBandsBatch.Update(series);
// Compare
Assert.Equal(iterativeMiddle.Count, batchMiddle.Count);
for (int i = 0; i < iterativeMiddle.Count; i++)
{
Assert.Equal(iterativeMiddle[i], batchMiddle[i].Value, 1e-10);
Assert.Equal(iterativeUpper[i], batchUpper[i].Value, 1e-10);
Assert.Equal(iterativeLower[i], batchLower[i].Value, 1e-10);
}
}
[Fact]
public void AccBands_NaN_Input_UsesLastValidValue()
{
var accBands = new AccBands(5);
// Feed some valid bars
accBands.Update(new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000));
accBands.Update(new TBar(DateTime.UtcNow, 102, 108, 98, 105, 1100));
// Feed bar with NaN high - should use last valid high
var resultAfterNaN = accBands.Update(new TBar(DateTime.UtcNow, 105, double.NaN, 100, 108, 1200));
// Result should be finite (not NaN)
Assert.True(double.IsFinite(resultAfterNaN.Value));
Assert.True(double.IsFinite(accBands.Upper.Value));
Assert.True(double.IsFinite(accBands.Lower.Value));
}
[Fact]
public void AccBands_Infinity_Input_UsesLastValidValue()
{
var accBands = new AccBands(5);
// Feed some valid bars
accBands.Update(new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000));
accBands.Update(new TBar(DateTime.UtcNow, 102, 108, 98, 105, 1100));
// Feed bar with positive infinity low
var resultAfterPosInf = accBands.Update(new TBar(DateTime.UtcNow, 105, 110, double.PositiveInfinity, 108, 1200));
Assert.True(double.IsFinite(resultAfterPosInf.Value));
Assert.True(double.IsFinite(accBands.Upper.Value));
Assert.True(double.IsFinite(accBands.Lower.Value));
// Feed bar with negative infinity close
var resultAfterNegInf = accBands.Update(new TBar(DateTime.UtcNow, 108, 115, 105, double.NegativeInfinity, 1300));
Assert.True(double.IsFinite(resultAfterNegInf.Value));
Assert.True(double.IsFinite(accBands.Upper.Value));
Assert.True(double.IsFinite(accBands.Lower.Value));
}
[Fact]
public void AccBands_MultipleNaN_ContinuesWithLastValid()
{
var accBands = new AccBands(5);
// Feed valid bars
accBands.Update(new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000));
accBands.Update(new TBar(DateTime.UtcNow, 102, 108, 98, 105, 1100));
accBands.Update(new TBar(DateTime.UtcNow, 105, 112, 100, 108, 1200));
// Feed multiple bars with NaN values
var r1 = accBands.Update(new TBar(DateTime.UtcNow, double.NaN, 115, 102, 110, 1300));
var r2 = accBands.Update(new TBar(DateTime.UtcNow, 110, double.NaN, 105, 112, 1400));
var r3 = accBands.Update(new TBar(DateTime.UtcNow, 112, 120, double.NaN, 115, 1500));
// All results should be finite
Assert.True(double.IsFinite(r1.Value));
Assert.True(double.IsFinite(r2.Value));
Assert.True(double.IsFinite(r3.Value));
}
[Fact]
public void AccBands_StaticBatch_Works()
{
var series = new TBarSeries();
series.Add(DateTime.UtcNow, 100, 110, 90, 100, 1000);
series.Add(DateTime.UtcNow, 105, 115, 95, 105, 1000);
series.Add(DateTime.UtcNow, 110, 120, 100, 110, 1000);
series.Add(DateTime.UtcNow, 115, 125, 105, 115, 1000);
series.Add(DateTime.UtcNow, 120, 130, 110, 120, 1000);
var (middle, upper, lower) = AccBands.Batch(series, 3);
Assert.Equal(5, middle.Count);
Assert.Equal(5, upper.Count);
Assert.Equal(5, lower.Count);
// All values should be finite
for (int i = 0; i < 5; i++)
{
Assert.True(double.IsFinite(middle[i].Value));
Assert.True(double.IsFinite(upper[i].Value));
Assert.True(double.IsFinite(lower[i].Value));
}
}
[Fact]
public void AccBands_Period1_ReturnsDirectCalculation()
{
var accBands = new AccBands(1);
// Single bar: H=110, L=90, C=100
// BandWidth = (110 - 90) * 2.0 = 40
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
// Next bar: H=120, L=100, C=110 (window is 1, so only this bar counts)
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
}
// ============== Span API Tests ==============
[Fact]
public void AccBands_SpanBatch_ValidatesInput()
{
double[] high = [105, 110, 115];
double[] low = [95, 100, 105];
double[] close = [100, 105, 110];
double[] middle = new double[3];
double[] upper = new double[3];
double[] lower = new double[3];
double[] wrongSizeHigh = [105, 110];
// Period must be > 0
Assert.Throws<ArgumentException>(() =>
AccBands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(),
middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 0));
Assert.Throws<ArgumentException>(() =>
AccBands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(),
middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), -1));
// Factor must be > 0
Assert.Throws<ArgumentException>(() =>
AccBands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(),
middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 3, 0));
Assert.Throws<ArgumentException>(() =>
AccBands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(),
middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 3, -1));
// Input arrays must have same length
Assert.Throws<ArgumentException>(() =>
AccBands.Batch(wrongSizeHigh.AsSpan(), low.AsSpan(), close.AsSpan(),
middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 3));
}
[Fact]
public void AccBands_SpanBatch_MatchesTSeriesBatch()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
var series = new TBarSeries();
double[] high = new double[100];
double[] low = new double[100];
double[] close = new double[100];
for (int i = 0; i < 100; i++)
{
var bar = gbm.Next(isNew: true);
series.Add(bar);
high[i] = bar.High;
low[i] = bar.Low;
close[i] = bar.Close;
}
// Calculate with TBarSeries API
var (tseriesMiddle, tseriesUpper, tseriesLower) = AccBands.Batch(series, 10);
// Calculate with Span API
double[] spanMiddle = new double[100];
double[] spanUpper = new double[100];
double[] spanLower = new double[100];
AccBands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(),
spanMiddle.AsSpan(), spanUpper.AsSpan(), spanLower.AsSpan(), 10);
// Compare results
for (int i = 0; i < 100; i++)
{
Assert.Equal(tseriesMiddle[i].Value, spanMiddle[i], 1e-10);
Assert.Equal(tseriesUpper[i].Value, spanUpper[i], 1e-10);
Assert.Equal(tseriesLower[i].Value, spanLower[i], 1e-10);
}
}
[Fact]
public void AccBands_SpanBatch_CalculatesCorrectly()
{
double[] high = [110, 115, 120, 125, 130];
double[] low = [90, 95, 100, 105, 110];
double[] close = [100, 105, 110, 115, 120];
double[] middle = new double[5];
double[] upper = new double[5];
double[] lower = new double[5];
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
// SMA(3) of Close: (100+105+110)/3 = 105
// BandWidth = (115-95) * 2.0 = 40
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
}
[Fact]
public void AccBands_SpanBatch_ZeroAllocation()
{
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
double[] high = new double[10000];
double[] low = new double[10000];
double[] close = new double[10000];
double[] middle = new double[10000];
double[] upper = new double[10000];
double[] lower = new double[10000];
for (int i = 0; i < high.Length; i++)
{
var bar = gbm.Next();
high[i] = bar.High;
low[i] = bar.Low;
close[i] = bar.Close;
}
// Warm up
AccBands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(),
middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 100);
// Verify method completes without OOM or stack overflow
Assert.True(double.IsFinite(middle[^1]));
Assert.True(double.IsFinite(upper[^1]));
Assert.True(double.IsFinite(lower[^1]));
}
[Fact]
public void AccBands_SpanBatch_HandlesNaN()
{
double[] high = [105, 110, double.NaN, 120, 125];
double[] low = [95, 100, 105, double.NaN, 115];
double[] close = [100, 105, 110, 115, double.NaN];
double[] middle = new double[5];
double[] upper = new double[5];
double[] lower = new double[5];
AccBands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(),
middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 3);
// All outputs should be finite
for (int i = 0; i < 5; i++)
{
Assert.True(double.IsFinite(middle[i]), $"Middle[{i}] expected finite but got {middle[i]}");
Assert.True(double.IsFinite(upper[i]), $"Upper[{i}] expected finite but got {upper[i]}");
Assert.True(double.IsFinite(lower[i]), $"Lower[{i}] expected finite but got {lower[i]}");
}
}
[Fact]
public void AccBands_AllModes_ProduceSameResult()
{
// Arrange
int period = 10;
double factor = 2.0;
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// 1. Batch Mode
var (batchMiddle, batchUpper, batchLower) = AccBands.Batch(bars, period, factor);
double expectedMiddle = batchMiddle.Last.Value;
double expectedUpper = batchUpper.Last.Value;
double expectedLower = batchLower.Last.Value;
// 2. Span Mode
double[] high = bars.HighValues.ToArray();
double[] low = bars.LowValues.ToArray();
double[] close = bars.CloseValues.ToArray();
double[] spanMiddle = new double[bars.Count];
double[] spanUpper = new double[bars.Count];
double[] spanLower = new double[bars.Count];
AccBands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(),
spanMiddle.AsSpan(), spanUpper.AsSpan(), spanLower.AsSpan(), period, factor);
// 3. Streaming Mode
var streamingInd = new AccBands(period, factor);
for (int i = 0; i < bars.Count; i++)
{
streamingInd.Update(bars[i]);
}
double streamingMiddle = streamingInd.Last.Value;
double streamingUpper = streamingInd.Upper.Value;
double streamingLower = streamingInd.Lower.Value;
// 4. Eventing Mode
var pubSource = new TBarSeries();
var eventingInd = new AccBands(pubSource, period, factor);
for (int i = 0; i < bars.Count; i++)
{
pubSource.Add(bars[i]);
}
double eventingMiddle = eventingInd.Last.Value;
double eventingUpper = eventingInd.Upper.Value;
double eventingLower = eventingInd.Lower.Value;
// Assert
Assert.Equal(expectedMiddle, spanMiddle[^1], precision: 9);
Assert.Equal(expectedUpper, spanUpper[^1], precision: 9);
Assert.Equal(expectedLower, spanLower[^1], precision: 9);
Assert.Equal(expectedMiddle, streamingMiddle, precision: 9);
Assert.Equal(expectedUpper, streamingUpper, precision: 9);
Assert.Equal(expectedLower, streamingLower, precision: 9);
Assert.Equal(expectedMiddle, eventingMiddle, precision: 9);
Assert.Equal(expectedUpper, eventingUpper, precision: 9);
Assert.Equal(expectedLower, eventingLower, precision: 9);
}
[Fact]
public void AccBands_Chainability_Works()
{
var source = new TBarSeries();
var accBands = new AccBands(source, 10);
source.Add(new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000));
Assert.Equal(102, accBands.Last.Value);
}
[Fact]
public void AccBands_WarmupPeriod_IsSetCorrectly()
{
var accBands = new AccBands(10);
Assert.Equal(10, accBands.WarmupPeriod);
}
[Fact]
public void AccBands_Prime_SetsStateCorrectly()
{
var accBands = new AccBands(3, 2.0);
var series = new TBarSeries();
// Add 5 bars
series.Add(DateTime.UtcNow, 100, 110, 90, 100, 1000);
series.Add(DateTime.UtcNow, 105, 115, 95, 105, 1000);
series.Add(DateTime.UtcNow, 110, 120, 100, 110, 1000);
series.Add(DateTime.UtcNow, 115, 125, 105, 115, 1000);
series.Add(DateTime.UtcNow, 120, 130, 110, 120, 1000);
accBands.Prime(series);
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
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
// 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
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
}
[Fact]
public void AccBands_Calculate_ReturnsCorrectResultsAndHotIndicator()
{
var series = new TBarSeries();
series.Add(DateTime.UtcNow, 100, 110, 90, 100, 1000);
series.Add(DateTime.UtcNow, 105, 115, 95, 105, 1000);
series.Add(DateTime.UtcNow, 110, 120, 100, 110, 1000);
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);
// Check results
Assert.Equal(5, middle.Count);
Assert.Equal(5, upper.Count);
Assert.Equal(5, lower.Count);
// Check indicator state
Assert.True(indicator.IsHot);
Assert.Equal(115.0, indicator.Last.Value, 1e-10);
Assert.Equal(3, indicator.WarmupPeriod);
// Verify indicator continues correctly
indicator.Update(new TBar(DateTime.UtcNow, 125, 135, 115, 125, 1000));
Assert.Equal(120.0, indicator.Last.Value, 1e-10);
}
[Fact]
public void AccBands_DifferentFactors_Work()
{
var series = new TBarSeries();
series.Add(DateTime.UtcNow, 100, 110, 90, 100, 1000);
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 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
Assert.Equal(middle1.Last.Value, middle3.Last.Value, 1e-10);
}
[Fact]
public void AccBands_FlatLine_ReturnsSameValues()
{
var accBands = new AccBands(10);
for (int i = 0; i < 20; i++)
{
accBands.Update(new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1000));
}
// When H=L=C=100, BandWidth = (100-100)*2 = 0
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
}
[Fact]
public void AccBands_Pub_EventFires()
{
var accBands = new AccBands(10);
bool eventFired = false;
accBands.Pub += (object? sender, in TValueEventArgs args) => eventFired = true;
accBands.Update(new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000));
Assert.True(eventFired);
}
}
@@ -0,0 +1,381 @@
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
/// <summary>
/// Validation tests for AccBands indicator.
/// Note: Skender.Stock.Indicators, TA-Lib, Tulip, and OoplesFinance do not provide
/// AccBands implementation for cross-validation. These tests validate against
/// manual calculations and internal consistency across all API modes.
/// </summary>
public sealed class AccBandsValidationTests : IDisposable
{
private readonly ValidationTestData _testData;
private readonly ITestOutputHelper _output;
private bool _disposed;
public AccBandsValidationTests(ITestOutputHelper output)
{
_output = output;
_testData = new ValidationTestData();
}
public void Dispose()
{
Dispose(true);
}
private void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{
_testData?.Dispose();
}
}
[Fact]
public void Validate_ManualCalculation_Period3()
{
// Manual calculation verification
// 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
var series = new TBarSeries();
var time = DateTime.UtcNow;
series.Add(new TBar(time, 10, 12, 8, 10, 100));
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 (middle, upper, lower) = accBands.Update(series);
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);
_output.WriteLine("AccBands manual calculation (period 3) validated successfully");
}
[Fact]
public void Validate_ManualCalculation_Period5()
{
// Manual calculation verification with period 5
var series = new TBarSeries();
var time = DateTime.UtcNow;
// Create predictable data: High = Close + 5, Low = Close - 5
double[] closes = { 100, 102, 104, 106, 108 };
for (int i = 0; i < closes.Length; i++)
{
double c = closes[i];
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 (middle, upper, lower) = accBands.Update(series);
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);
_output.WriteLine("AccBands manual calculation (period 5) validated successfully");
}
[Fact]
public void Validate_Factor_Effect()
{
// Verify factor affects band width correctly
var series = new TBarSeries();
var time = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
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
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);
// Middle should be the same regardless of factor
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
// 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
// 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
_output.WriteLine("AccBands factor effect validated successfully");
}
[Fact]
public void Validate_AllModes_Consistency_Batch()
{
int[] periods = { 5, 10, 20, 50, 100 };
foreach (var period in periods)
{
// Batch mode using instance
var accBands = new AccBands(period, 2.0);
var (qMiddle, qUpper, qLower) = accBands.Update(_testData.Bars);
// Static batch
var (sMiddle, sUpper, sLower) = AccBands.Batch(_testData.Bars, period, 2.0);
// Verify match
ValidationHelper.VerifySeriesEqual(qMiddle, sMiddle);
ValidationHelper.VerifySeriesEqual(qUpper, sUpper);
ValidationHelper.VerifySeriesEqual(qLower, sLower);
}
_output.WriteLine("AccBands Batch modes consistency validated successfully");
}
[Fact]
public void Validate_AllModes_Consistency_Streaming()
{
int[] periods = { 5, 10, 20, 50, 100 };
foreach (var period in periods)
{
// Streaming mode
var streamingAcc = new AccBands(period, 2.0);
var streamMiddle = new TSeries();
var streamUpper = new TSeries();
var streamLower = new TSeries();
foreach (var bar in _testData.Bars)
{
streamingAcc.Update(bar);
streamMiddle.Add(streamingAcc.Last);
streamUpper.Add(streamingAcc.Upper);
streamLower.Add(streamingAcc.Lower);
}
// Batch mode for comparison
var (batchMiddle, batchUpper, batchLower) = AccBands.Batch(_testData.Bars, period, 2.0);
// Verify match
ValidationHelper.VerifySeriesEqual(batchMiddle, streamMiddle);
ValidationHelper.VerifySeriesEqual(batchUpper, streamUpper);
ValidationHelper.VerifySeriesEqual(batchLower, streamLower);
}
_output.WriteLine("AccBands Streaming mode consistency validated successfully");
}
[Fact]
public void Validate_AllModes_Consistency_Span()
{
int[] periods = { 5, 10, 20, 50, 100 };
double[] high = _testData.HighPrices.ToArray();
double[] low = _testData.LowPrices.ToArray();
double[] close = _testData.ClosePrices.ToArray();
foreach (var period in periods)
{
// Span mode
int len = close.Length;
double[] spanMiddle = new double[len];
double[] spanUpper = new double[len];
double[] spanLower = new double[len];
AccBands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(),
spanMiddle.AsSpan(), spanUpper.AsSpan(), spanLower.AsSpan(),
period, 2.0);
// Batch mode for comparison
var (batchMiddle, batchUpper, batchLower) = AccBands.Batch(_testData.Bars, period, 2.0);
// Verify match
for (int i = 0; i < len; i++)
{
Assert.Equal(batchMiddle[i].Value, spanMiddle[i], 9);
Assert.Equal(batchUpper[i].Value, spanUpper[i], 9);
Assert.Equal(batchLower[i].Value, spanLower[i], 9);
}
}
_output.WriteLine("AccBands Span mode consistency validated successfully");
}
[Fact]
public void Validate_AllModes_Consistency_Eventing()
{
int[] periods = { 5, 10, 20, 50 };
foreach (var period in periods)
{
// Eventing mode
var pubSource = new TBarSeries();
var eventingInd = new AccBands(pubSource, period, 2.0);
var eventMiddle = new TSeries();
var eventUpper = new TSeries();
var eventLower = new TSeries();
foreach (var bar in _testData.Bars)
{
pubSource.Add(bar);
eventMiddle.Add(eventingInd.Last);
eventUpper.Add(eventingInd.Upper);
eventLower.Add(eventingInd.Lower);
}
// Batch mode for comparison
var (batchMiddle, batchUpper, batchLower) = AccBands.Batch(_testData.Bars, period, 2.0);
// Verify match
ValidationHelper.VerifySeriesEqual(batchMiddle, eventMiddle);
ValidationHelper.VerifySeriesEqual(batchUpper, eventUpper);
ValidationHelper.VerifySeriesEqual(batchLower, eventLower);
}
_output.WriteLine("AccBands Eventing mode consistency validated successfully");
}
[Fact]
public void Validate_Calculate_ReturnsHotIndicator()
{
int[] periods = { 5, 10, 20, 50, 100 };
foreach (var period in periods)
{
var ((middle, upper, lower), indicator) = AccBands.Calculate(_testData.Bars, period, 2.0);
// Verify indicator is hot
Assert.True(indicator.IsHot);
Assert.Equal(period, indicator.WarmupPeriod);
// Verify results match indicator state
Assert.Equal(middle.Last.Value, indicator.Last.Value, 1e-10);
Assert.Equal(upper.Last.Value, indicator.Upper.Value, 1e-10);
Assert.Equal(lower.Last.Value, indicator.Lower.Value, 1e-10);
// Verify can continue streaming
var nextBar = new TBar(DateTime.UtcNow.AddDays(1), 100, 110, 90, 105, 1000);
indicator.Update(nextBar);
Assert.True(indicator.IsHot);
}
_output.WriteLine("AccBands Calculate method validated successfully");
}
[Fact]
public void Validate_LargeDataset_NoOverflow()
{
// Test with the full 5000 bar dataset
var (middle, upper, lower) = AccBands.Batch(_testData.Bars, 100, 2.0);
// All outputs should be finite
ValidationHelper.VerifyAllFinite(middle, startIndex: 0);
ValidationHelper.VerifyAllFinite(upper, startIndex: 0);
ValidationHelper.VerifyAllFinite(lower, startIndex: 0);
// Upper should always be >= Middle, Middle should always be >= Lower (for normal data)
for (int i = 100; i < middle.Count; i++)
{
Assert.True(upper[i].Value >= middle[i].Value,
$"Upper ({upper[i].Value}) should be >= Middle ({middle[i].Value}) at index {i}");
Assert.True(middle[i].Value >= lower[i].Value,
$"Middle ({middle[i].Value}) should be >= Lower ({lower[i].Value}) at index {i}");
}
_output.WriteLine("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 global::QuanTAlib.Sma(20);
var smaLow = new global::QuanTAlib.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()
{
// Prime with history and verify state matches full calculation
int period = 20;
// Full batch calculation
var (batchMiddle, batchUpper, batchLower) = AccBands.Batch(_testData.Bars, period, 2.0);
// Prime indicator with subset and continue
var primedIndicator = new AccBands(period, 2.0);
var subset = new TBarSeries();
for (int i = 0; i < 100; i++)
{
subset.Add(_testData.Bars[i]);
}
primedIndicator.Prime(subset);
// Continue streaming from where Prime left off
for (int i = 100; i < _testData.Bars.Count; i++)
{
primedIndicator.Update(_testData.Bars[i]);
}
// Final values should match
Assert.Equal(batchMiddle.Last.Value, primedIndicator.Last.Value, 1e-9);
Assert.Equal(batchUpper.Last.Value, primedIndicator.Upper.Value, 1e-9);
Assert.Equal(batchLower.Last.Value, primedIndicator.Lower.Value, 1e-9);
_output.WriteLine("AccBands Prime method validated successfully");
}
}
+783
View File
@@ -0,0 +1,783 @@
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// AccBands: Acceleration Bands
/// </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.
///
/// Calculation:
/// 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:
/// - Bands expand during volatile periods and contract during consolidation
/// - Uses SMA of High, Low, and Close for calculations
/// - Factor parameter controls band sensitivity
///
/// Sources:
/// Headley, P. (2002). Big Trends in Trading. John Wiley & Sons.
/// </remarks>
[SkipLocalsInit]
public sealed class AccBands : ITValuePublisher
{
private readonly int _period;
private readonly double _factor;
private readonly RingBuffer _highBuffer;
private readonly RingBuffer _lowBuffer;
private readonly RingBuffer _closeBuffer;
private readonly TBarPublishedHandler _barHandler;
private const int ResyncInterval = 1000;
[StructLayout(LayoutKind.Auto)]
private record struct State(
double SumHigh,
double SumLow,
double SumClose,
double LastValidHigh,
double LastValidLow,
double LastValidClose,
int TickCount
);
private State _state;
private State _p_state;
/// <summary>
/// Display name for the indicator.
/// </summary>
public string Name { get; }
/// <summary>
/// Number of periods before the indicator is considered "hot" (valid).
/// </summary>
public int WarmupPeriod { get; }
/// <summary>
/// Current middle band value.
/// </summary>
public TValue Last { get; private set; }
/// <summary>
/// Current upper band value.
/// </summary>
public TValue Upper { get; private set; }
/// <summary>
/// Current lower band value.
/// </summary>
public TValue Lower { get; private set; }
/// <summary>
/// True if the indicator has enough data to produce valid results.
/// </summary>
public bool IsHot => _closeBuffer.IsFull;
/// <summary>
/// Event triggered when a new TValue is available.
/// </summary>
public event TValuePublishedHandler? Pub;
/// <summary>
/// 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)
{
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
if (factor <= 0)
throw new ArgumentException("Factor must be greater than 0", nameof(factor));
_period = period;
_factor = factor;
_highBuffer = new RingBuffer(period);
_lowBuffer = new RingBuffer(period);
_closeBuffer = new RingBuffer(period);
Name = $"AccBands({period},{factor:F2})";
WarmupPeriod = period;
_barHandler = HandleBar;
}
/// <summary>
/// Creates AccBands with TBarSeries source.
/// </summary>
public AccBands(TBarSeries source, int period, double factor = 2.0) : this(period, factor)
{
Prime(source);
source.Pub += _barHandler;
}
private void HandleBar(object? sender, in TBarEventArgs e) => Update(e.Value, e.IsNew);
/// <summary>
/// Helper to invoke the Pub event.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void PubEvent(TValue value, bool isNew = true)
{
Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew });
}
/// <summary>
/// Gets a valid input value, using last-value substitution for non-finite inputs.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidHigh(double input)
{
if (double.IsFinite(input))
{
_state.LastValidHigh = input;
return input;
}
return _state.LastValidHigh;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidLow(double input)
{
if (double.IsFinite(input))
{
_state.LastValidLow = input;
return input;
}
return _state.LastValidLow;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidClose(double input)
{
if (double.IsFinite(input))
{
_state.LastValidClose = input;
return input;
}
return _state.LastValidClose;
}
[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;
double removedClose = _closeBuffer.Count == _closeBuffer.Capacity ? _closeBuffer.Oldest : 0.0;
_state.SumHigh = _state.SumHigh - removedHigh + high;
_state.SumLow = _state.SumLow - removedLow + low;
_state.SumClose = _state.SumClose - removedClose + close;
_highBuffer.Add(high);
_lowBuffer.Add(low);
_closeBuffer.Add(close);
_state.TickCount++;
if (_closeBuffer.IsFull && _state.TickCount >= ResyncInterval)
{
_state.TickCount = 0;
_state.SumHigh = _highBuffer.RecalculateSum();
_state.SumLow = _lowBuffer.RecalculateSum();
_state.SumClose = _closeBuffer.RecalculateSum();
}
}
/// <summary>
/// Updates the indicator with a TBar input.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
double high = GetValidHigh(input.High);
double low = GetValidLow(input.Low);
double close = GetValidClose(input.Close);
UpdateState(high, low, close);
}
else
{
_state = _p_state;
double high = GetValidHigh(input.High);
double low = GetValidLow(input.Low);
double close = GetValidClose(input.Close);
_highBuffer.UpdateNewest(high);
_lowBuffer.UpdateNewest(low);
_closeBuffer.UpdateNewest(close);
_state = _state with
{
SumHigh = _highBuffer.Sum,
SumLow = _lowBuffer.Sum,
SumClose = _closeBuffer.Sum
};
}
int count = _closeBuffer.Count;
if (count == 0)
{
Last = new TValue(input.Time, double.NaN);
Upper = new TValue(input.Time, double.NaN);
Lower = new TValue(input.Time, double.NaN);
}
else
{
double smaHigh = _state.SumHigh / count;
double smaLow = _state.SumLow / 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);
}
PubEvent(Last, isNew);
return Last;
}
/// <summary>
/// Updates the indicator with a TBarSeries.
/// </summary>
public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TBarSeries source)
{
if (source.Count == 0)
return (new TSeries([], []), new TSeries([], []), new TSeries([], []));
int len = source.Count;
var tMiddle = new List<long>(len);
var vMiddle = new List<double>(len);
var tUpper = new List<long>(len);
var vUpper = new List<double>(len);
var tLower = new List<long>(len);
var vLower = new List<double>(len);
CollectionsMarshal.SetCount(tMiddle, len);
CollectionsMarshal.SetCount(vMiddle, len);
CollectionsMarshal.SetCount(tUpper, len);
CollectionsMarshal.SetCount(vUpper, len);
CollectionsMarshal.SetCount(tLower, len);
CollectionsMarshal.SetCount(vLower, len);
var tSpan = CollectionsMarshal.AsSpan(tMiddle);
var vMiddleSpan = CollectionsMarshal.AsSpan(vMiddle);
var vUpperSpan = CollectionsMarshal.AsSpan(vUpper);
var vLowerSpan = CollectionsMarshal.AsSpan(vLower);
// Use batch calculation
Batch(source.HighValues, source.LowValues, source.CloseValues,
vMiddleSpan, vUpperSpan, vLowerSpan, _period, _factor);
source.Times.CopyTo(tSpan);
// Copy timestamps to upper and lower (same time series)
tSpan.CopyTo(CollectionsMarshal.AsSpan(tUpper));
tSpan.CopyTo(CollectionsMarshal.AsSpan(tLower));
// Prime the state for continued streaming
Prime(source);
return (new TSeries(tMiddle, vMiddle), new TSeries(tUpper, vUpper), new TSeries(tLower, vLower));
}
/// <summary>
/// Initializes the indicator state using the provided TBarSeries history.
/// </summary>
// skipcq: CS-R1140
public void Prime(TBarSeries source)
{
if (source.Count == 0) return;
// Reset state
_highBuffer.Clear();
_lowBuffer.Clear();
_closeBuffer.Clear();
_state = default;
_p_state = default;
int warmupLength = Math.Min(source.Count, WarmupPeriod);
int startIndex = source.Count - warmupLength;
// Seed LastValidValue
_state.LastValidHigh = double.NaN;
_state.LastValidLow = double.NaN;
_state.LastValidClose = double.NaN;
for (int i = startIndex - 1; i >= 0; i--)
{
var bar = source[i];
if (double.IsFinite(bar.High) && double.IsNaN(_state.LastValidHigh))
_state.LastValidHigh = bar.High;
if (double.IsFinite(bar.Low) && double.IsNaN(_state.LastValidLow))
_state.LastValidLow = bar.Low;
if (double.IsFinite(bar.Close) && double.IsNaN(_state.LastValidClose))
_state.LastValidClose = bar.Close;
if (!double.IsNaN(_state.LastValidHigh) && !double.IsNaN(_state.LastValidLow) && !double.IsNaN(_state.LastValidClose))
break;
}
// Find valid values in warmup window if not found
if (double.IsNaN(_state.LastValidHigh) || double.IsNaN(_state.LastValidLow) || double.IsNaN(_state.LastValidClose))
{
for (int i = startIndex; i < source.Count; i++)
{
var bar = source[i];
if (double.IsFinite(bar.High) && double.IsNaN(_state.LastValidHigh))
_state.LastValidHigh = bar.High;
if (double.IsFinite(bar.Low) && double.IsNaN(_state.LastValidLow))
_state.LastValidLow = bar.Low;
if (double.IsFinite(bar.Close) && double.IsNaN(_state.LastValidClose))
_state.LastValidClose = bar.Close;
if (!double.IsNaN(_state.LastValidHigh) && !double.IsNaN(_state.LastValidLow) && !double.IsNaN(_state.LastValidClose))
break;
}
}
// Feed the buffers
for (int i = startIndex; i < source.Count; i++)
{
var bar = source[i];
double high = GetValidHigh(bar.High);
double low = GetValidLow(bar.Low);
double close = GetValidClose(bar.Close);
UpdateState(high, low, close);
}
// Finalize state
int count = _closeBuffer.Count;
if (count > 0)
{
var lastBar = source.Last;
double smaHigh = _state.SumHigh / count;
double smaLow = _state.SumLow / 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);
}
_p_state = _state;
}
/// <summary>
/// Resets the indicator state.
/// </summary>
public void Reset()
{
_highBuffer.Clear();
_lowBuffer.Clear();
_closeBuffer.Clear();
_state = default;
_p_state = default;
Last = default;
Upper = default;
Lower = default;
}
/////////////////////////////////////////////////////////////////////////////////////////////////
// Static Batch Methods
/////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary>
/// Output buffers for batch AccBands calculation.
/// </summary>
/// <remarks>
/// Public Span fields are intentional: ref structs cannot use auto-properties with Span&lt;T&gt;
/// and direct field access provides optimal performance for this high-throughput API.
/// </remarks>
[StructLayout(LayoutKind.Auto)]
#pragma warning disable S1104 // Fields should not have public accessibility
public ref struct BatchOutputs
{
/// <summary>Output middle band (SMA of close)</summary>
public Span<double> Middle;
/// <summary>Output upper band</summary>
public Span<double> Upper;
/// <summary>Output lower band</summary>
public Span<double> Lower;
#pragma warning restore S1104
/// <summary>
/// Creates a new BatchOutputs instance.
/// </summary>
public BatchOutputs(Span<double> middle, Span<double> upper, Span<double> lower)
{
Middle = middle;
Upper = upper;
Lower = lower;
}
}
/// <summary>
/// Input buffers for batch AccBands calculation.
/// </summary>
[StructLayout(LayoutKind.Auto)]
#pragma warning disable S1104 // Fields should not have public accessibility
public ref struct BatchInputs
{
/// <summary>High price values</summary>
public ReadOnlySpan<double> High;
/// <summary>Low price values</summary>
public ReadOnlySpan<double> Low;
/// <summary>Close price values</summary>
public ReadOnlySpan<double> Close;
#pragma warning restore S1104
/// <summary>
/// Creates a new BatchInputs instance.
/// </summary>
public BatchInputs(ReadOnlySpan<double> high, ReadOnlySpan<double> low, ReadOnlySpan<double> close)
{
High = high;
Low = low;
Close = close;
}
}
/// <summary>
/// Internal state for scalar calculation.
/// </summary>
[StructLayout(LayoutKind.Auto)]
private ref struct ScalarState
{
public double SumHigh;
public double SumLow;
public double SumClose;
public double LastValidHigh;
public double LastValidLow;
public double LastValidClose;
public int BufferIndex;
public int TickCount;
}
/// <summary>
/// Working buffers for batch calculation.
/// </summary>
[StructLayout(LayoutKind.Auto)]
private ref struct WorkBuffers
{
public Span<double> High;
public Span<double> Low;
public Span<double> Close;
public WorkBuffers(Span<double> high, Span<double> low, Span<double> close)
{
High = high;
Low = low;
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)
{
var accBands = new AccBands(period, factor);
return accBands.Update(source);
}
/// <summary>
/// Calculates AccBands in-place using spans for maximum performance.
/// Zero-allocation method.
/// </summary>
/// <param name="inputs">Input buffers for high, low, and close prices</param>
/// <param name="outputs">Output buffers for middle, upper, and lower bands</param>
/// <param name="period">Lookback period</param>
/// <param name="factor">Band width factor</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(
BatchInputs inputs,
BatchOutputs outputs,
int period,
double factor = 2.0)
{
Batch(inputs.High, inputs.Low, inputs.Close, outputs.Middle, outputs.Upper, outputs.Lower, period, factor);
}
/// <summary>
/// Calculates AccBands in-place using spans for maximum performance.
/// Zero-allocation method.
/// </summary>
/// <param name="high">High price values</param>
/// <param name="low">Low price values</param>
/// <param name="close">Close price values</param>
/// <param name="outputs">Output buffers for middle, upper, and lower bands</param>
/// <param name="period">Lookback period</param>
/// <param name="factor">Band width factor</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(
ReadOnlySpan<double> high,
ReadOnlySpan<double> low,
ReadOnlySpan<double> close,
BatchOutputs outputs,
int period,
double factor = 2.0)
{
Batch(high, low, close, outputs.Middle, outputs.Upper, outputs.Lower, period, factor);
}
/// <summary>
/// Calculates AccBands in-place using spans for maximum performance.
/// Zero-allocation method.
/// </summary>
/// <param name="high">High price values</param>
/// <param name="low">Low price values</param>
/// <param name="close">Close price values</param>
/// <param name="middle">Output middle band (SMA of close)</param>
/// <param name="upper">Output upper band</param>
/// <param name="lower">Output lower band</param>
/// <param name="period">Lookback period</param>
/// <param name="factor">Band width factor</param>
// Suppressing S107: This is a high-performance batch API where callers benefit from
// direct span parameters. A BatchOutputs overload exists for callers preferring fewer parameters.
// Suppressing S3776: The cognitive complexity is required for SIMD optimization paths,
// NaN handling, warmup logic, and buffer management. Extracting these to separate methods
// would harm performance (prevent inlining) and reduce maintainability (breaks the
// cohesive calculation flow). The method is well-structured with clear helper methods.
#pragma warning disable S107, S3776
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(
ReadOnlySpan<double> high,
ReadOnlySpan<double> low,
ReadOnlySpan<double> close,
Span<double> middle,
Span<double> upper,
Span<double> lower,
int period,
double factor = 2.0)
#pragma warning restore S107, S3776
{
int len = close.Length;
if (high.Length != len || low.Length != len)
throw new ArgumentException("High, Low, and Close must have the same length", nameof(high));
if (middle.Length < len || upper.Length < len || lower.Length < len)
throw new ArgumentException("Output buffers must be at least as long as input", nameof(middle));
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
if (factor <= 0)
throw new ArgumentException("Factor must be greater than 0", nameof(factor));
if (len == 0) return;
// Scalar implementation with NaN handling
var inputs = new BatchInputs(high, low, close);
var outputs = new BatchOutputs(middle, upper, lower);
CalculateScalarCore(inputs, outputs, period, factor);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CalculateScalarCore(
scoped BatchInputs inputs,
scoped BatchOutputs outputs,
int period,
double factor)
{
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[] rentedClose = ArrayPool<double>.Shared.Rent(period);
try
{
var buffers = new WorkBuffers(
rentedHigh.AsSpan(0, period),
rentedLow.AsSpan(0, period),
rentedClose.AsSpan(0, period));
var state = new ScalarState
{
LastValidHigh = double.NaN,
LastValidLow = double.NaN,
LastValidClose = double.NaN
};
SeedFirstValidValues(inputs, ref state);
int warmupEnd = Math.Min(period, len);
ProcessWarmupPhase(inputs, outputs, warmupEnd, factor, ref buffers, ref state);
ProcessMainLoop(inputs, outputs, warmupEnd, period, factor, ref buffers, ref state);
}
finally
{
ArrayPool<double>.Shared.Return(rentedHigh);
ArrayPool<double>.Shared.Return(rentedLow);
ArrayPool<double>.Shared.Return(rentedClose);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void SeedFirstValidValues(scoped BatchInputs inputs, ref ScalarState state)
{
int len = inputs.Close.Length;
for (int k = 0; k < len; k++)
{
if (double.IsFinite(inputs.High[k]) && double.IsNaN(state.LastValidHigh))
state.LastValidHigh = inputs.High[k];
if (double.IsFinite(inputs.Low[k]) && double.IsNaN(state.LastValidLow))
state.LastValidLow = inputs.Low[k];
if (double.IsFinite(inputs.Close[k]) && double.IsNaN(state.LastValidClose))
state.LastValidClose = inputs.Close[k];
if (!double.IsNaN(state.LastValidHigh) && !double.IsNaN(state.LastValidLow) && !double.IsNaN(state.LastValidClose))
break;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static (double h, double l, double c) GetValidHLC(scoped BatchInputs inputs, int i, ref ScalarState state)
{
double h = inputs.High[i];
double l = inputs.Low[i];
double c = inputs.Close[i];
if (double.IsFinite(h)) state.LastValidHigh = h; else h = state.LastValidHigh;
if (double.IsFinite(l)) state.LastValidLow = l; else l = state.LastValidLow;
if (double.IsFinite(c)) state.LastValidClose = c; else c = state.LastValidClose;
return (h, l, c);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteBandOutputs(scoped BatchOutputs outputs, int i, double smaHigh, double smaLow, double smaClose, double factor)
{
double bandWidth = (smaHigh - smaLow) * factor;
outputs.Middle[i] = smaClose;
outputs.Upper[i] = smaHigh + bandWidth;
outputs.Lower[i] = smaLow - bandWidth;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ProcessWarmupPhase(
scoped BatchInputs inputs,
scoped BatchOutputs outputs,
int warmupEnd,
double factor,
ref WorkBuffers buffers,
ref ScalarState state)
{
for (int i = 0; i < warmupEnd; i++)
{
var (h, l, c) = GetValidHLC(inputs, i, ref state);
state.SumHigh += h;
state.SumLow += l;
state.SumClose += c;
buffers.High[i] = h;
buffers.Low[i] = l;
buffers.Close[i] = c;
int count = i + 1;
WriteBandOutputs(outputs, i, state.SumHigh / count, state.SumLow / count, state.SumClose / count, factor);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ProcessMainLoop(
scoped BatchInputs inputs,
scoped BatchOutputs outputs,
int startIndex,
int period,
double factor,
ref WorkBuffers buffers,
ref ScalarState state)
{
int len = inputs.Close.Length;
for (int i = startIndex; i < len; i++)
{
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;
state.SumClose = state.SumClose - buffers.Close[state.BufferIndex] + c;
buffers.High[state.BufferIndex] = h;
buffers.Low[state.BufferIndex] = l;
buffers.Close[state.BufferIndex] = c;
state.BufferIndex++;
if (state.BufferIndex >= period) state.BufferIndex = 0;
WriteBandOutputs(outputs, i, state.SumHigh / period, state.SumLow / period, state.SumClose / period, factor);
state.TickCount++;
if (state.TickCount >= ResyncInterval)
{
ResyncSums(period, ref buffers, ref state);
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ResyncSums(int period, ref WorkBuffers buffers, ref ScalarState state)
{
state.TickCount = 0;
if (Vector.IsHardwareAccelerated && period >= Vector<double>.Count)
{
state.SumHigh = SumSimd(buffers.High);
state.SumLow = SumSimd(buffers.Low);
state.SumClose = SumSimd(buffers.Close);
}
else
{
double recalcSumHigh = 0, recalcSumLow = 0, recalcSumClose = 0;
for (int k = 0; k < period; k++)
{
recalcSumHigh += buffers.High[k];
recalcSumLow += buffers.Low[k];
recalcSumClose += buffers.Close[k];
}
state.SumHigh = recalcSumHigh;
state.SumLow = recalcSumLow;
state.SumClose = recalcSumClose;
}
}
private static double SumSimd(ReadOnlySpan<double> source)
{
var sumVector = Vector<double>.Zero;
int i = 0;
int size = Vector<double>.Count;
int len = source.Length;
for (; i <= len - size; i += size)
{
sumVector += new Vector<double>(source.Slice(i, size));
}
double sum = Vector.Sum(sumVector);
for (; i < len; i++)
{
sum += source[i];
}
return sum;
}
/// <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)
{
var accBands = new AccBands(period, factor);
var results = accBands.Update(source);
return (results, accBands);
}
}
+73
View File
@@ -0,0 +1,73 @@
# ACCBANDS: Acceleration Bands
## Overview and Purpose
Acceleration Bands are a volatility-based indicator developed by Price Headley that creates an adaptive price envelope around a moving average. Unlike static percentage-based bands, Acceleration Bands dynamically adjust their width based on the spread between the high and low moving averages, making them responsive to changing market conditions. This approach allows the bands to expand during volatile periods and contract during consolidation, providing traders with a visual representation of potential support and resistance levels that adapt to market volatility.
The implementation provided uses efficient circular buffers for SMA calculations, ensuring optimal performance while properly handling data gaps. By creating a channel that widens during increased volatility and narrows during reduced volatility, Acceleration Bands offer traders a framework for identifying potential reversal points and measuring trend strength based on a security's natural price rhythm rather than arbitrary fixed percentages.
## Core Concepts
* **Volatility-adaptive channels:** Bands automatically widen during volatile markets and narrow during calm periods
* **Moving average foundation:** Uses simple moving averages of high, low, and close prices as the basis for calculations
* **Dynamic bandwidth:** Band width determined by the difference between high and low SMAs, adjusted by a multiplier
* **Symmetrical envelope:** Equal expansion above and below the centerline for balanced support/resistance identification
Acceleration Bands stand apart from other channel indicators by directly incorporating the natural range of price movement (high-low differential) into their width calculation. This creates a more market-adaptive envelope that responds to the inherent volatility characteristics of each security, rather than applying a uniform volatility measure across different instruments.
## Common Settings and Parameters
| Parameter | Default | Function | When to Adjust |
| --------- | ------- | -------- | -------------- |
| Period | 20 | Lookback period for all SMA calculations | Shorter for more sensitivity to recent price action; longer for smoother, less reactive bands |
| Factor | 2.0 | Multiplier for band width | Higher values for wider bands that trigger fewer signals; lower values for tighter bands with more frequent signals |
| Sources | High, Low, Close | Price data components | Rarely needs adjustment unless analyzing specific price aspects |
**Pro Tip:** Try using a band factor of 1.0 for shorter-term trading and 2.0-3.0 for longer-term analysis. The sweet spot often lies where the bands contain approximately 85-90% of price action, with only significant moves breaking beyond the bands.
## Calculation and Mathematical Foundation
**Simplified explanation:**
Acceleration Bands calculate a middle line as the SMA of closing prices, then create upper and lower bands by adding or subtracting the high-low differential (multiplied by a factor) to or from this middle line.
**Technical formula:**
Middle Band = SMA(Close, Period)
Upper Band = SMA(High, Period) + [SMA(High, Period) - SMA(Low, Period)] × Factor
Lower Band = SMA(Low, Period) - [SMA(High, Period) - SMA(Low, Period)] × Factor
Where:
* SMA = Simple Moving Average
* Period = Lookback period for calculations
* Factor = Multiplier for the band width
> 🔍 **Technical Note:** The implementation uses circular buffers to efficiently maintain running sums for all three SMAs (high, low, close), ensuring O(1) computational complexity regardless of the lookback period. This approach prevents recalculating entire sums each bar while properly handling NA values that may appear in the source data.
## Interpretation Details
Acceleration Bands provide several analytical perspectives:
* **Overbought/oversold conditions:** Price reaching or exceeding the upper band suggests potentially overbought conditions; touching or breaking below the lower band indicates potentially oversold conditions
* **Trend strength assessment:** Price persistently touching or moving beyond the bands in the direction of the trend indicates strong momentum
* **Volatility measurement:** The distance between bands provides a visual representation of current market volatility
* **Support and resistance levels:** During uptrends, the middle and lower bands often act as support; during downtrends, the middle and upper bands frequently serve as resistance
* **Mean reversion signals:** Moves beyond the bands followed by reversals back inside often signal potential mean reversion opportunities
* **Convergence/divergence patterns:** Narrowing bands indicate decreasing volatility, often preceding significant price moves; widening bands suggest increasing volatility
## Limitations and Considerations
* **Lagging component:** As a moving average-based indicator, Acceleration Bands exhibit some lag, potentially missing the initial stages of significant moves
* **Parameter sensitivity:** Results can vary significantly based on period and factor settings
* **False signals:** During strong trends, the bands may generate false reversal signals
* **Ineffectiveness in trendless markets:** May produce excessive signals in consolidating or choppy markets
* **Extreme volatility handling:** During periods of extremely high volatility, the bands may widen excessively, reducing their usefulness for near-term reversal identification
* **Complementary tool:** Works best when combined with other technical indicators for confirmation
* **Timeframe dependence:** Optimal parameters vary across different timeframes
## References
* Headley, P. (2002). Big Trends in Trading: Strategies for Maximum Market Returns. John Wiley & Sons.
* Kaufman, P. J. (2013). Trading Systems and Methods (5th ed.). John Wiley & Sons.
* Murphy, J. J. (1999). Technical Analysis of the Financial Markets. New York Institute of Finance.
* Pring, M. J. (2002). Technical Analysis Explained. McGraw-Hill.
+64
View File
@@ -0,0 +1,64 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Acceleration Bands (ACCBANDS)", "ACCBANDS", overlay=true)
//@function Calculates Acceleration Bands using SMAs of high, low, close prices
//@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
//@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) =>
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> bufferClose = array.new_float(p, na)
var float sumHigh = 0.0
var float sumLow = 0.0
var float sumClose = 0.0
float oldestHigh = array.get(bufferHigh, head)
float oldestLow = array.get(bufferLow, head)
float oldestClose = array.get(bufferClose, head)
if not na(oldestHigh)
sumHigh -= oldestHigh
sumLow -= oldestLow
sumClose -= oldestClose
count -= 1
float currentHigh = nz(high)
float currentLow = nz(low)
float currentClose = nz(close)
sumHigh += currentHigh
sumLow += currentLow
sumClose += currentClose
count += 1
array.set(bufferHigh, head, currentHigh)
array.set(bufferLow, head, currentLow)
array.set(bufferClose, head, currentClose)
head := (head + 1) % p
float smaHigh = nz(sumHigh / count)
float smaLow = nz(sumLow / count)
float smaClose = nz(sumClose / count)
float bandWidth = (smaHigh - smaLow) * factor
[smaClose, smaHigh + bandWidth, smaLow - bandWidth]
// ---------- Main loop ----------
// Inputs
i_period = input.int(20, "Period", minval=1)
i_factor = input.float(2.0, "Factor", minval=0.001)
// Calculation
[middle, upper, lower] = accbands(high, low, close, i_period, i_factor)
// Plot
plot(middle, "Middle", color=color.yellow, linewidth=2)
p1 = plot(upper, "Upper", color=color.yellow, linewidth=2)
p2 = plot(lower, "Lower", color=color.yellow, linewidth=2)
fill(p1, p2, color=color.new(color.blue, 90), title="Band Fill")