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