mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-20 11:38:05 +00:00
Remove multiple Pine Script indicators: SSFDSP, STARCHANNEL, STBANDS, STC, UBANDS, UCHANNEL, VWAPBANDS, and VWAPSD. These indicators were deleted to streamline the library and remove unused or redundant code.
This commit is contained in:
@@ -144,28 +144,27 @@ public class AbberTests
|
||||
var abber = new Abber(3, 2.0);
|
||||
|
||||
// Bar 1: source = 100
|
||||
// SMA = 100, Deviation = 0, AvgDev = 0
|
||||
// SMA = 100, Deviation = |100-100| = 0, AvgDev = 0
|
||||
abber.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.Equal(100.0, abber.Last.Value, 1e-10);
|
||||
|
||||
// Bar 2: source = 110
|
||||
// SMA(2) = (100+110)/2 = 105
|
||||
// Dev1 = |100 - 100| = 0 (calculated when 100 was added, SMA was 100)
|
||||
// Dev2 = |110 - 100| = 10 (calculated when 110 is added, SMA was 100)
|
||||
// AvgDev = (0+10)/2 = 5
|
||||
// Upper = 105 + 2*5 = 115, Lower = 105 - 2*5 = 95
|
||||
// Dev1 = 0, Dev2 = |110 - 105| = 5 (same-bar SMA)
|
||||
// AvgDev = (0+5)/2 = 2.5
|
||||
// Upper = 105 + 2*2.5 = 110, Lower = 105 - 2*2.5 = 100
|
||||
abber.Update(new TValue(DateTime.UtcNow, 110));
|
||||
Assert.Equal(105.0, abber.Last.Value, 1e-10);
|
||||
|
||||
// Bar 3: source = 120
|
||||
// SMA(3) = (100+110+120)/3 = 110
|
||||
// Dev3 = |120 - 105| = 15 (calculated when 120 is added, SMA was 105)
|
||||
// AvgDev = (0+10+15)/3 = 8.333...
|
||||
// Upper = 110 + 2*8.333 = 126.666..., Lower = 110 - 2*8.333 = 93.333...
|
||||
// Dev3 = |120 - 110| = 10 (same-bar SMA)
|
||||
// AvgDev = (0+5+10)/3 = 5.0
|
||||
// Upper = 110 + 2*5 = 120, Lower = 110 - 2*5 = 100
|
||||
abber.Update(new TValue(DateTime.UtcNow, 120));
|
||||
Assert.Equal(110.0, abber.Last.Value, 1e-10);
|
||||
Assert.Equal(110.0 + 2.0 * 25.0 / 3.0, abber.Upper.Value, 1e-10);
|
||||
Assert.Equal(110.0 - 2.0 * 25.0 / 3.0, abber.Lower.Value, 1e-10);
|
||||
Assert.Equal(120.0, abber.Upper.Value, 1e-10);
|
||||
Assert.Equal(100.0, abber.Lower.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -36,11 +36,11 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
|
||||
[Fact]
|
||||
public void Validate_ManualCalculation_Period3()
|
||||
{
|
||||
// Manual calculation verification
|
||||
// Manual calculation verification (same-bar SMA deviation)
|
||||
// Values: [100, 110, 120]
|
||||
// Bar 1: SMA=100, Dev=0, AvgDev=0
|
||||
// Bar 2: SMA=(100+110)/2=105, Dev1=0, Dev2=|110-100|=10, AvgDev=(0+10)/2=5
|
||||
// Bar 3: SMA=(100+110+120)/3=110, Dev3=|120-105|=15, AvgDev=(0+10+15)/3=8.333
|
||||
// Bar 1: SMA=100, Dev=|100-100|=0, AvgDev=0
|
||||
// Bar 2: SMA=(100+110)/2=105, Dev2=|110-105|=5, AvgDev=(0+5)/2=2.5
|
||||
// Bar 3: SMA=(100+110+120)/3=110, Dev3=|120-110|=10, AvgDev=(0+5+10)/3=5.0
|
||||
|
||||
var series = new TSeries();
|
||||
var time = DateTime.UtcNow;
|
||||
@@ -54,8 +54,8 @@ public sealed class AbberValidationTests(ITestOutputHelper output) : IDisposable
|
||||
// SMA(3) = 110
|
||||
Assert.Equal(110.0, middle.Last.Value, 1e-10);
|
||||
|
||||
// AvgDev = (0 + 10 + 15) / 3 = 25/3
|
||||
const double expectedAvgDev = 25.0 / 3.0;
|
||||
// AvgDev = (0 + 5 + 10) / 3 = 5.0
|
||||
const double expectedAvgDev = 5.0;
|
||||
double expectedBandWidth = 2.0 * expectedAvgDev;
|
||||
|
||||
Assert.Equal(110.0 + expectedBandWidth, upper.Last.Value, 1e-10);
|
||||
|
||||
+27
-19
@@ -190,9 +190,12 @@ public sealed class Abber : ITValuePublisher, IDisposable
|
||||
{
|
||||
_pState = _state;
|
||||
|
||||
// Calculate SMA first to get deviation
|
||||
// Compute SMA including the new value with correct divisor (matches batch ProcessMainLoop)
|
||||
int count = _sourceBuffer.Count;
|
||||
double sma = count > 0 ? _state.SumSource / count : value;
|
||||
double removedSource = count == _sourceBuffer.Capacity ? _sourceBuffer.Oldest : 0.0;
|
||||
double newSum = _state.SumSource - removedSource + value;
|
||||
int newCount = count < _sourceBuffer.Capacity ? count + 1 : count;
|
||||
double sma = newSum / newCount;
|
||||
double deviation = Math.Abs(value - sma);
|
||||
|
||||
UpdateState(value, deviation);
|
||||
@@ -201,17 +204,18 @@ public sealed class Abber : ITValuePublisher, IDisposable
|
||||
{
|
||||
_state = _pState;
|
||||
|
||||
// Calculate SMA first to get deviation
|
||||
int count = _sourceBuffer.Count;
|
||||
double sma = count > 0 ? _state.SumSource / count : value;
|
||||
double deviation = Math.Abs(value - sma);
|
||||
|
||||
// Replace newest source value and recompute sum for current-bar SMA (matches Pine)
|
||||
_sourceBuffer.UpdateNewest(value);
|
||||
_deviationBuffer.UpdateNewest(deviation);
|
||||
double currentSum = _sourceBuffer.Sum;
|
||||
int corrCount = _sourceBuffer.Count;
|
||||
double corrSma = corrCount > 0 ? currentSum / corrCount : value;
|
||||
double corrDeviation = Math.Abs(value - corrSma);
|
||||
|
||||
_deviationBuffer.UpdateNewest(corrDeviation);
|
||||
|
||||
_state = _state with
|
||||
{
|
||||
SumSource = _sourceBuffer.Sum,
|
||||
SumSource = currentSum,
|
||||
SumDeviation = _deviationBuffer.Sum,
|
||||
};
|
||||
}
|
||||
@@ -357,9 +361,12 @@ public sealed class Abber : ITValuePublisher, IDisposable
|
||||
{
|
||||
double value = GetValidValue(source[i].Value);
|
||||
|
||||
// Calculate SMA to get deviation
|
||||
// Compute SMA including the new value with correct divisor (matches batch ProcessMainLoop)
|
||||
int count = _sourceBuffer.Count;
|
||||
double sma = count > 0 ? _state.SumSource / count : value;
|
||||
double removedSource = count == _sourceBuffer.Capacity ? _sourceBuffer.Oldest : 0.0;
|
||||
double newSum = _state.SumSource - removedSource + value;
|
||||
int newCount = count < _sourceBuffer.Capacity ? count + 1 : count;
|
||||
double sma = newSum / newCount;
|
||||
double deviation = Math.Abs(value - sma);
|
||||
|
||||
UpdateState(value, deviation);
|
||||
@@ -606,18 +613,18 @@ public sealed class Abber : ITValuePublisher, IDisposable
|
||||
{
|
||||
double v = GetValidValue(source, i, ref state);
|
||||
|
||||
// Calculate current SMA to get deviation
|
||||
int count = i;
|
||||
double sma = count > 0 ? state.SumSource / count : v;
|
||||
// Compute SMA including the new value to get same-bar deviation (matches Pine)
|
||||
int newCount = i + 1;
|
||||
double newSum = state.SumSource + v;
|
||||
double sma = newSum / newCount;
|
||||
double deviation = Math.Abs(v - sma);
|
||||
|
||||
state.SumSource += v;
|
||||
state.SumSource = newSum;
|
||||
state.SumDeviation += deviation;
|
||||
|
||||
buffers.Source[i] = v;
|
||||
buffers.Deviation[i] = deviation;
|
||||
|
||||
int newCount = i + 1;
|
||||
double middle = state.SumSource / newCount;
|
||||
double avgDeviation = state.SumDeviation / newCount;
|
||||
WriteBandOutputs(outputs, i, middle, avgDeviation, multiplier);
|
||||
@@ -639,12 +646,13 @@ public sealed class Abber : ITValuePublisher, IDisposable
|
||||
{
|
||||
double v = GetValidValue(source, i, ref state);
|
||||
|
||||
// Calculate current SMA to get deviation
|
||||
double sma = state.SumSource / period;
|
||||
// Compute SMA including the new value to get same-bar deviation (matches Pine)
|
||||
double newSumSource = state.SumSource - buffers.Source[state.BufferIndex] + v;
|
||||
double sma = newSumSource / period;
|
||||
double deviation = Math.Abs(v - sma);
|
||||
|
||||
// Update running sums using single buffer index
|
||||
state.SumSource = state.SumSource - buffers.Source[state.BufferIndex] + v;
|
||||
state.SumSource = newSumSource;
|
||||
buffers.Source[state.BufferIndex] = v;
|
||||
|
||||
state.SumDeviation = state.SumDeviation - buffers.Deviation[state.BufferIndex] + deviation;
|
||||
|
||||
@@ -11,7 +11,7 @@ public class AccBandsIndicatorTests
|
||||
var indicator = new AccBandsIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(2.0, indicator.Factor);
|
||||
Assert.Equal(4.0, indicator.Factor);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("AccBands - Acceleration Bands", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
@@ -123,7 +123,7 @@ public class AccBandsIndicatorTests
|
||||
[Fact]
|
||||
public void BandRelationship_UpperAboveLowerBelowMiddle()
|
||||
{
|
||||
var indicator = new AccBandsIndicator { Period = 5, Factor = 2.0 };
|
||||
var indicator = new AccBandsIndicator { Period = 5, Factor = 4.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
@@ -147,12 +147,12 @@ public class AccBandsIndicatorTests
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Narrow bands with factor 1.0
|
||||
var narrowIndicator = new AccBandsIndicator { Period = 5, Factor = 1.0 };
|
||||
// Narrow bands with factor 2.0
|
||||
var narrowIndicator = new AccBandsIndicator { Period = 5, Factor = 2.0 };
|
||||
narrowIndicator.Initialize();
|
||||
|
||||
// Wide bands with factor 3.0
|
||||
var wideIndicator = new AccBandsIndicator { Period = 5, Factor = 3.0 };
|
||||
// Wide bands with factor 6.0
|
||||
var wideIndicator = new AccBandsIndicator { Period = 5, Factor = 6.0 };
|
||||
wideIndicator.Initialize();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
@@ -183,8 +183,8 @@ public class AccBandsIndicatorTests
|
||||
[Fact]
|
||||
public void Factor_CanBeChanged()
|
||||
{
|
||||
var indicator = new AccBandsIndicator { Factor = 2.0 };
|
||||
Assert.Equal(2.0, indicator.Factor);
|
||||
var indicator = new AccBandsIndicator { Factor = 4.0 };
|
||||
Assert.Equal(4.0, indicator.Factor);
|
||||
|
||||
indicator.Factor = 3.5;
|
||||
Assert.Equal(3.5, indicator.Factor);
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace QuanTAlib;
|
||||
/// <summary>
|
||||
/// AccBands: Acceleration Bands - Quantower Indicator Adapter
|
||||
/// Volatility-based channel indicator developed by Price Headley that creates
|
||||
/// an adaptive price envelope around a moving average.
|
||||
/// an adaptive price envelope using per-bar normalized width adjustment.
|
||||
/// </summary>
|
||||
public sealed class AccBandsIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
@@ -17,7 +17,7 @@ public sealed class AccBandsIndicator : Indicator, IWatchlistIndicator
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[InputParameter("Factor", sortIndex: 11, minimum: 0.1, maximum: 10.0, increment: 0.1, decimalPlaces: 2)]
|
||||
public double Factor { get; set; } = 2.0;
|
||||
public double Factor { get; set; } = 4.0;
|
||||
|
||||
[InputParameter("Show Cold Values", sortIndex: 100)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
@@ -30,7 +30,7 @@ public sealed class AccBandsIndicator : Indicator, IWatchlistIndicator
|
||||
public AccBandsIndicator()
|
||||
{
|
||||
Name = "AccBands - Acceleration Bands";
|
||||
Description = "Volatility-based adaptive price channel using SMA of High, Low, and Close";
|
||||
Description = "Volatility-based adaptive price channel using per-bar normalized width (Headley)";
|
||||
SeparateWindow = false;
|
||||
OnBackGround = true;
|
||||
}
|
||||
|
||||
@@ -45,15 +45,16 @@ public class AccBandsTests
|
||||
var accBands = new AccBands(10);
|
||||
|
||||
// First bar: O=100, H=105, L=95, C=102
|
||||
// SMA of one value: high=105, low=95, close=102
|
||||
// BandWidth = (105 - 95) * 2.0 = 20
|
||||
// Middle = 102, Upper = 105 + 20 = 125, Lower = 95 - 20 = 75
|
||||
// w = (105-95)/(105+95) = 10/200 = 0.05
|
||||
// adjHigh = 105 * (1 + 4*0.05) = 105 * 1.2 = 126
|
||||
// adjLow = 95 * (1 - 4*0.05) = 95 * 0.8 = 76
|
||||
// Middle = 102, Upper = 126, Lower = 76
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
accBands.Update(bar);
|
||||
|
||||
Assert.Equal(102.0, accBands.Last.Value, 1e-10);
|
||||
Assert.Equal(125.0, accBands.Upper.Value, 1e-10);
|
||||
Assert.Equal(75.0, accBands.Lower.Value, 1e-10);
|
||||
Assert.Equal(126.0, accBands.Upper.Value, 1e-10);
|
||||
Assert.Equal(76.0, accBands.Lower.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -156,31 +157,40 @@ public class AccBandsTests
|
||||
[Fact]
|
||||
public void AccBands_CalculatesCorrectBands()
|
||||
{
|
||||
var accBands = new AccBands(3, 2.0);
|
||||
var accBands = new AccBands(3, 4.0);
|
||||
|
||||
// Bar 1: H=110, L=90, C=100
|
||||
// w1 = (110-90)/(110+90) = 20/200 = 0.1
|
||||
// adjH1 = 110*(1+4*0.1) = 110*1.4 = 154
|
||||
// adjL1 = 90*(1-4*0.1) = 90*0.6 = 54
|
||||
accBands.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000));
|
||||
// Bar 2: H=115, L=95, C=105
|
||||
// w2 = (115-95)/(115+95) = 20/210 ≈ 0.095238
|
||||
// adjH2 = 115*(1+4*0.095238) = 115*1.380952 ≈ 158.80952
|
||||
// adjL2 = 95*(1-4*0.095238) = 95*0.619048 ≈ 58.80952
|
||||
accBands.Update(new TBar(DateTime.UtcNow, 105, 115, 95, 105, 1000));
|
||||
// Bar 3: H=120, L=100, C=110
|
||||
// w3 = (120-100)/(120+100) = 20/220 ≈ 0.090909
|
||||
// adjH3 = 120*(1+4*0.090909) = 120*1.363636 ≈ 163.63636
|
||||
// adjL3 = 100*(1-4*0.090909) = 100*0.636364 ≈ 63.63636
|
||||
accBands.Update(new TBar(DateTime.UtcNow, 110, 120, 100, 110, 1000));
|
||||
|
||||
// SMA(3) of High: (110 + 115 + 120) / 3 = 115
|
||||
// SMA(3) of Low: (90 + 95 + 100) / 3 = 95
|
||||
// SMA(3) of Close: (100 + 105 + 110) / 3 = 105
|
||||
// BandWidth = (115 - 95) * 2.0 = 40
|
||||
// Upper = 115 + 40 = 155
|
||||
// Lower = 95 - 40 = 55
|
||||
// SMA(3) of adjHigh: (154 + 158.80952 + 163.63636) / 3 ≈ 158.81529
|
||||
// SMA(3) of adjLow: (54 + 58.80952 + 63.63636) / 3 ≈ 58.81529
|
||||
// SMA(3) of Close: (100+105+110)/3 = 105
|
||||
|
||||
double expectedUpper = (154.0 + 115.0 * (1.0 + 4.0 * 20.0 / 210.0) + 120.0 * (1.0 + 4.0 * 20.0 / 220.0)) / 3.0;
|
||||
double expectedLower = (54.0 + 95.0 * (1.0 - 4.0 * 20.0 / 210.0) + 100.0 * (1.0 - 4.0 * 20.0 / 220.0)) / 3.0;
|
||||
|
||||
Assert.Equal(105.0, accBands.Last.Value, 1e-10);
|
||||
Assert.Equal(155.0, accBands.Upper.Value, 1e-10);
|
||||
Assert.Equal(55.0, accBands.Lower.Value, 1e-10);
|
||||
Assert.Equal(expectedUpper, accBands.Upper.Value, 1e-10);
|
||||
Assert.Equal(expectedLower, accBands.Lower.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AccBands_SlidingWindow_Works()
|
||||
{
|
||||
var accBands = new AccBands(3, 2.0);
|
||||
var accBands = new AccBands(3, 4.0);
|
||||
|
||||
// Bar 1: H=110, L=90, C=100
|
||||
accBands.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000));
|
||||
@@ -191,20 +201,18 @@ public class AccBandsTests
|
||||
|
||||
double middle1 = accBands.Last.Value;
|
||||
|
||||
// Bar 4: H=125, L=105, C=115 - Window slides: [115, 120, 125], [95, 100, 105], [105, 110, 115]
|
||||
// Bar 4: H=125, L=105, C=115 - Window slides to bars [2,3,4]
|
||||
// w4 = (125-105)/(125+105) = 20/230 ≈ 0.086957
|
||||
// adjH4 = 125*(1+4*0.086957) = 125*1.347826 ≈ 168.47826
|
||||
// adjL4 = 105*(1-4*0.086957) = 105*0.652174 ≈ 68.47826
|
||||
accBands.Update(new TBar(DateTime.UtcNow, 115, 125, 105, 115, 1000));
|
||||
|
||||
// SMA(3) of High: (115 + 120 + 125) / 3 = 120
|
||||
// SMA(3) of Low: (95 + 100 + 105) / 3 = 100
|
||||
// SMA(3) of Close: (105 + 110 + 115) / 3 = 110
|
||||
// BandWidth = (120 - 100) * 2.0 = 40
|
||||
// Upper = 120 + 40 = 160
|
||||
// Lower = 100 - 40 = 60
|
||||
|
||||
Assert.NotEqual(middle1, accBands.Last.Value);
|
||||
Assert.Equal(110.0, accBands.Last.Value, 1e-10);
|
||||
Assert.Equal(160.0, accBands.Upper.Value, 1e-10);
|
||||
Assert.Equal(60.0, accBands.Lower.Value, 1e-10);
|
||||
|
||||
// Verify Upper > Middle > Lower
|
||||
Assert.True(accBands.Upper.Value > accBands.Last.Value);
|
||||
Assert.True(accBands.Lower.Value < accBands.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -377,17 +385,22 @@ public class AccBandsTests
|
||||
var accBands = new AccBands(1);
|
||||
|
||||
// Single bar: H=110, L=90, C=100
|
||||
// BandWidth = (110 - 90) * 2.0 = 40
|
||||
// w = (110-90)/(110+90) = 20/200 = 0.1
|
||||
// adjHigh = 110*(1+4*0.1) = 110*1.4 = 154
|
||||
// adjLow = 90*(1-4*0.1) = 90*0.6 = 54
|
||||
accBands.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000));
|
||||
Assert.Equal(100.0, accBands.Last.Value, 1e-10);
|
||||
Assert.Equal(150.0, accBands.Upper.Value, 1e-10); // 110 + 40
|
||||
Assert.Equal(50.0, accBands.Lower.Value, 1e-10); // 90 - 40
|
||||
Assert.Equal(154.0, accBands.Upper.Value, 1e-10);
|
||||
Assert.Equal(54.0, accBands.Lower.Value, 1e-10);
|
||||
|
||||
// Next bar: H=120, L=100, C=110 (window is 1, so only this bar counts)
|
||||
// w = (120-100)/(120+100) = 20/220 ≈ 0.090909
|
||||
// adjHigh = 120*(1+4*0.090909) = 120*1.363636 ≈ 163.63636
|
||||
// adjLow = 100*(1-4*0.090909) = 100*0.636364 ≈ 63.63636
|
||||
accBands.Update(new TBar(DateTime.UtcNow, 110, 120, 100, 110, 1000));
|
||||
Assert.Equal(110.0, accBands.Last.Value, 1e-10);
|
||||
Assert.Equal(160.0, accBands.Upper.Value, 1e-10); // 120 + 40
|
||||
Assert.Equal(60.0, accBands.Lower.Value, 1e-10); // 100 - 40
|
||||
Assert.Equal(120.0 * (1.0 + 4.0 * 20.0 / 220.0), accBands.Upper.Value, 1e-10);
|
||||
Assert.Equal(100.0 * (1.0 - 4.0 * 20.0 / 220.0), accBands.Lower.Value, 1e-10);
|
||||
}
|
||||
|
||||
// ============== Span API Tests ==============
|
||||
@@ -477,14 +490,22 @@ public class AccBandsTests
|
||||
AccBands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(),
|
||||
middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 3);
|
||||
|
||||
// After warmup (index 2):
|
||||
// SMA(3) of High: (110+115+120)/3 = 115
|
||||
// SMA(3) of Low: (90+95+100)/3 = 95
|
||||
// After warmup (index 2): bars 0,1,2
|
||||
// Bar 0: H=110, L=90 => w=20/200=0.1, adjH=110*1.4=154, adjL=90*0.6=54
|
||||
// Bar 1: H=115, L=95 => w=20/210, adjH=115*(1+4*20/210), adjL=95*(1-4*20/210)
|
||||
// Bar 2: H=120, L=100 => w=20/220, adjH=120*(1+4*20/220), adjL=100*(1-4*20/220)
|
||||
// SMA(3) of Close: (100+105+110)/3 = 105
|
||||
// BandWidth = (115-95) * 2.0 = 40
|
||||
|
||||
double adjH0 = 110.0 * (1.0 + 4.0 * 20.0 / 200.0);
|
||||
double adjH1 = 115.0 * (1.0 + 4.0 * 20.0 / 210.0);
|
||||
double adjH2 = 120.0 * (1.0 + 4.0 * 20.0 / 220.0);
|
||||
double adjL0 = 90.0 * (1.0 - 4.0 * 20.0 / 200.0);
|
||||
double adjL1 = 95.0 * (1.0 - 4.0 * 20.0 / 210.0);
|
||||
double adjL2 = 100.0 * (1.0 - 4.0 * 20.0 / 220.0);
|
||||
|
||||
Assert.Equal(105.0, middle[2], 1e-10);
|
||||
Assert.Equal(155.0, upper[2], 1e-10); // 115 + 40
|
||||
Assert.Equal(55.0, lower[2], 1e-10); // 95 - 40
|
||||
Assert.Equal((adjH0 + adjH1 + adjH2) / 3.0, upper[2], 1e-10);
|
||||
Assert.Equal((adjL0 + adjL1 + adjL2) / 3.0, lower[2], 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -543,7 +564,7 @@ public class AccBandsTests
|
||||
{
|
||||
// Arrange
|
||||
const int period = 10;
|
||||
double factor = 2.0;
|
||||
double factor = 4.0;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
@@ -618,7 +639,7 @@ public class AccBandsTests
|
||||
[Fact]
|
||||
public void AccBands_Prime_SetsStateCorrectly()
|
||||
{
|
||||
var accBands = new AccBands(3, 2.0);
|
||||
var accBands = new AccBands(3, 4.0);
|
||||
var series = new TBarSeries();
|
||||
|
||||
// Add 5 bars
|
||||
@@ -632,25 +653,31 @@ public class AccBandsTests
|
||||
|
||||
Assert.True(accBands.IsHot);
|
||||
|
||||
// Last 3 bars: H=[120,125,130], L=[100,105,110], C=[110,115,120]
|
||||
// SMA(3) of High: (120+125+130)/3 = 125
|
||||
// SMA(3) of Low: (100+105+110)/3 = 105
|
||||
// SMA(3) of Close: (110+115+120)/3 = 115
|
||||
// BandWidth = (125-105) * 2.0 = 40
|
||||
// Last 3 bars: bars 2,3,4
|
||||
// Bar 2: H=120,L=100,C=110 -> w=20/220, adjH=120*(1+80/220), adjL=100*(1-80/220)
|
||||
// Bar 3: H=125,L=105,C=115 -> w=20/230, adjH=125*(1+80/230), adjL=105*(1-80/230)
|
||||
// Bar 4: H=130,L=110,C=120 -> w=20/240, adjH=130*(1+80/240), adjL=110*(1-80/240)
|
||||
double adjH2 = 120.0 * (1.0 + 4.0 * 20.0 / 220.0);
|
||||
double adjL2 = 100.0 * (1.0 - 4.0 * 20.0 / 220.0);
|
||||
double adjH3 = 125.0 * (1.0 + 4.0 * 20.0 / 230.0);
|
||||
double adjL3 = 105.0 * (1.0 - 4.0 * 20.0 / 230.0);
|
||||
double adjH4 = 130.0 * (1.0 + 4.0 * 20.0 / 240.0);
|
||||
double adjL4 = 110.0 * (1.0 - 4.0 * 20.0 / 240.0);
|
||||
|
||||
Assert.Equal(115.0, accBands.Last.Value, 1e-10);
|
||||
Assert.Equal(165.0, accBands.Upper.Value, 1e-10); // 125 + 40
|
||||
Assert.Equal(65.0, accBands.Lower.Value, 1e-10); // 105 - 40
|
||||
Assert.Equal((adjH2 + adjH3 + adjH4) / 3.0, accBands.Upper.Value, 1e-10);
|
||||
Assert.Equal((adjL2 + adjL3 + adjL4) / 3.0, accBands.Lower.Value, 1e-10);
|
||||
|
||||
// Verify it continues correctly
|
||||
accBands.Update(new TBar(DateTime.UtcNow, 125, 135, 115, 125, 1000));
|
||||
// New window: H=[125,130,135], L=[105,110,115], C=[115,120,125]
|
||||
// SMA(3) of High: (125+130+135)/3 = 130
|
||||
// SMA(3) of Low: (105+110+115)/3 = 110
|
||||
// SMA(3) of Close: (115+120+125)/3 = 120
|
||||
// BandWidth = (130-110) * 2.0 = 40
|
||||
// New window: bars [3,4,5]
|
||||
// Bar 5: H=135,L=115,C=125 -> w=20/250, adjH=135*(1+80/250), adjL=115*(1-80/250)
|
||||
double adjH5 = 135.0 * (1.0 + 4.0 * 20.0 / 250.0);
|
||||
double adjL5 = 115.0 * (1.0 - 4.0 * 20.0 / 250.0);
|
||||
|
||||
Assert.Equal(120.0, accBands.Last.Value, 1e-10);
|
||||
Assert.Equal(170.0, accBands.Upper.Value, 1e-10); // 130 + 40
|
||||
Assert.Equal(70.0, accBands.Lower.Value, 1e-10); // 110 - 40
|
||||
Assert.Equal((adjH3 + adjH4 + adjH5) / 3.0, accBands.Upper.Value, 1e-10);
|
||||
Assert.Equal((adjL3 + adjL4 + adjL5) / 3.0, accBands.Lower.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -663,7 +690,7 @@ public class AccBandsTests
|
||||
series.Add(DateTime.UtcNow, 115, 125, 105, 115, 1000);
|
||||
series.Add(DateTime.UtcNow, 120, 130, 110, 120, 1000);
|
||||
|
||||
var ((middle, upper, lower), indicator) = AccBands.Calculate(series, 3, 2.0);
|
||||
var ((middle, upper, lower), indicator) = AccBands.Calculate(series, 3, 4.0);
|
||||
|
||||
// Check results
|
||||
Assert.Equal(5, middle.Count);
|
||||
@@ -688,20 +715,18 @@ public class AccBandsTests
|
||||
series.Add(DateTime.UtcNow, 105, 115, 95, 105, 1000);
|
||||
series.Add(DateTime.UtcNow, 110, 120, 100, 110, 1000);
|
||||
|
||||
// Factor 1.0
|
||||
var (middle1, upper1, lower1) = AccBands.Batch(series, 3, 1.0);
|
||||
// SMA(3) High=115, Low=95, Close=105, BandWidth=20*1=20
|
||||
Assert.Equal(135.0, upper1.Last.Value, 1e-10); // 115 + 20
|
||||
Assert.Equal(75.0, lower1.Last.Value, 1e-10); // 95 - 20
|
||||
// Factor 2.0
|
||||
var (middle1, upper1, lower1) = AccBands.Batch(series, 3, 2.0);
|
||||
// Factor 6.0
|
||||
var (middle3, upper3, lower3) = AccBands.Batch(series, 3, 6.0);
|
||||
|
||||
// Factor 3.0
|
||||
var (middle3, upper3, lower3) = AccBands.Batch(series, 3, 3.0);
|
||||
// BandWidth=20*3=60
|
||||
Assert.Equal(175.0, upper3.Last.Value, 1e-10); // 115 + 60
|
||||
Assert.Equal(35.0, lower3.Last.Value, 1e-10); // 95 - 60
|
||||
|
||||
// Middle should be the same for all factors
|
||||
// Middle should be the same regardless of factor (SMA of close)
|
||||
Assert.Equal(middle1.Last.Value, middle3.Last.Value, 1e-10);
|
||||
|
||||
// Wider factor = wider bands
|
||||
double width1 = upper1.Last.Value - lower1.Last.Value;
|
||||
double width3 = upper3.Last.Value - lower3.Last.Value;
|
||||
Assert.True(width3 > width1, $"Factor 6 width ({width3}) should be > factor 2 width ({width1})");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -714,10 +739,11 @@ public class AccBandsTests
|
||||
accBands.Update(new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1000));
|
||||
}
|
||||
|
||||
// When H=L=C=100, BandWidth = (100-100)*2 = 0
|
||||
// When H=L=C=100, w = (100-100)/(100+100) = 0
|
||||
// adjHigh = 100*(1+0) = 100, adjLow = 100*(1-0) = 100
|
||||
Assert.Equal(100.0, accBands.Last.Value, 1e-10);
|
||||
Assert.Equal(100.0, accBands.Upper.Value, 1e-10); // 100 + 0
|
||||
Assert.Equal(100.0, accBands.Lower.Value, 1e-10); // 100 - 0
|
||||
Assert.Equal(100.0, accBands.Upper.Value, 1e-10);
|
||||
Assert.Equal(100.0, accBands.Lower.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -5,10 +5,10 @@ namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for AccBands indicator.
|
||||
/// Note: TA-Lib provides ACCBANDS but uses a different formula (per-bar adaptive width
|
||||
/// via High*(1+4*(H-L)/(H+L))) whereas QuanTAlib uses SMA-based band width.
|
||||
/// The middle band (SMA of Close) matches exactly between both implementations.
|
||||
/// Skender, Tulip, and OoplesFinance do not provide AccBands.
|
||||
/// Now using Headley's original formula: Upper = SMA(High*(1+factor*(H-L)/(H+L))),
|
||||
/// Lower = SMA(Low*(1-factor*(H-L)/(H+L))), Middle = SMA(Close).
|
||||
/// TA-Lib uses the same per-bar Headley formula with factor=4, so all three bands
|
||||
/// should match exactly. Skender, Tulip, and OoplesFinance do not provide AccBands.
|
||||
/// </summary>
|
||||
public sealed class AccBandsValidationTests : IDisposable
|
||||
{
|
||||
@@ -45,15 +45,14 @@ public sealed class AccBandsValidationTests : IDisposable
|
||||
[Fact]
|
||||
public void Validate_ManualCalculation_Period3()
|
||||
{
|
||||
// Manual calculation verification
|
||||
// Manual calculation verification with Headley's formula
|
||||
// Given: High = [12, 14, 16], Low = [8, 10, 12], Close = [10, 12, 14]
|
||||
// SMA(High, 3) = (12 + 14 + 16) / 3 = 14
|
||||
// SMA(Low, 3) = (8 + 10 + 12) / 3 = 10
|
||||
// SMA(Close, 3) = (10 + 12 + 14) / 3 = 12
|
||||
// BandWidth = (14 - 10) * 2.0 = 8
|
||||
// Upper = 14 + 8 = 22
|
||||
// Lower = 10 - 8 = 2
|
||||
// Middle = 12
|
||||
// Bar 0: w=4/20=0.2, adjH=12*(1+4*0.2)=12*1.8=21.6, adjL=8*(1-4*0.2)=8*0.2=1.6
|
||||
// Bar 1: w=4/24≈0.16667, adjH=14*(1+4/6)=14*1.66667≈23.33333, adjL=10*(1-4/6)=10*0.33333≈3.33333
|
||||
// Bar 2: w=4/28≈0.14286, adjH=16*(1+4*4/28)=16*1.57143≈25.14286, adjL=12*(1-4*4/28)=12*0.42857≈5.14286
|
||||
// SMA(3) Middle = (10+12+14)/3 = 12
|
||||
// SMA(3) Upper = (21.6 + 23.33333 + 25.14286) / 3
|
||||
// SMA(3) Lower = (1.6 + 3.33333 + 5.14286) / 3
|
||||
|
||||
var series = new TBarSeries();
|
||||
var time = DateTime.UtcNow;
|
||||
@@ -61,12 +60,19 @@ public sealed class AccBandsValidationTests : IDisposable
|
||||
series.Add(new TBar(time.AddMinutes(1), 12, 14, 10, 12, 100));
|
||||
series.Add(new TBar(time.AddMinutes(2), 14, 16, 12, 14, 100));
|
||||
|
||||
var accBands = new AccBands(3, 2.0);
|
||||
var accBands = new AccBands(3, 4.0);
|
||||
var (middle, upper, lower) = accBands.Update(series);
|
||||
|
||||
double adjH0 = 12.0 * (1.0 + 4.0 * 4.0 / 20.0);
|
||||
double adjH1 = 14.0 * (1.0 + 4.0 * 4.0 / 24.0);
|
||||
double adjH2 = 16.0 * (1.0 + 4.0 * 4.0 / 28.0);
|
||||
double adjL0 = 8.0 * (1.0 - 4.0 * 4.0 / 20.0);
|
||||
double adjL1 = 10.0 * (1.0 - 4.0 * 4.0 / 24.0);
|
||||
double adjL2 = 12.0 * (1.0 - 4.0 * 4.0 / 28.0);
|
||||
|
||||
Assert.Equal(12.0, middle.Last.Value, 1e-10);
|
||||
Assert.Equal(22.0, upper.Last.Value, 1e-10);
|
||||
Assert.Equal(2.0, lower.Last.Value, 1e-10);
|
||||
Assert.Equal((adjH0 + adjH1 + adjH2) / 3.0, upper.Last.Value, 1e-10);
|
||||
Assert.Equal((adjL0 + adjL1 + adjL2) / 3.0, lower.Last.Value, 1e-10);
|
||||
|
||||
_output.WriteLine("AccBands manual calculation (period 3) validated successfully");
|
||||
}
|
||||
@@ -74,7 +80,7 @@ public sealed class AccBandsValidationTests : IDisposable
|
||||
[Fact]
|
||||
public void Validate_ManualCalculation_Period5()
|
||||
{
|
||||
// Manual calculation verification with period 5
|
||||
// Manual calculation verification with period 5, Headley formula
|
||||
var series = new TBarSeries();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
@@ -86,19 +92,25 @@ public sealed class AccBandsValidationTests : IDisposable
|
||||
series.Add(new TBar(time.AddMinutes(i), c, c + 5, c - 5, c, 1000));
|
||||
}
|
||||
|
||||
// SMA(High, 5) = (105 + 107 + 109 + 111 + 113) / 5 = 109
|
||||
// SMA(Low, 5) = (95 + 97 + 99 + 101 + 103) / 5 = 99
|
||||
// SMA(Close, 5) = (100 + 102 + 104 + 106 + 108) / 5 = 104
|
||||
// BandWidth = (109 - 99) * 2.0 = 20
|
||||
// Upper = 109 + 20 = 129
|
||||
// Lower = 99 - 20 = 79
|
||||
|
||||
var accBands = new AccBands(5, 2.0);
|
||||
var accBands = new AccBands(5, 4.0);
|
||||
var (middle, upper, lower) = accBands.Update(series);
|
||||
|
||||
// SMA(Close, 5) = (100 + 102 + 104 + 106 + 108) / 5 = 104
|
||||
Assert.Equal(104.0, middle.Last.Value, 1e-10);
|
||||
Assert.Equal(129.0, upper.Last.Value, 1e-10);
|
||||
Assert.Equal(79.0, lower.Last.Value, 1e-10);
|
||||
|
||||
// Each bar: H=c+5, L=c-5, w=10/(2c), adjH=(c+5)*(1+40/(2c)), adjL=(c-5)*(1-40/(2c))
|
||||
double sumAdjH = 0, sumAdjL = 0;
|
||||
foreach (double c in closes)
|
||||
{
|
||||
double h = c + 5;
|
||||
double l = c - 5;
|
||||
double denom = h + l;
|
||||
double w = (h - l) / denom;
|
||||
sumAdjH += h * (1.0 + 4.0 * w);
|
||||
sumAdjL += l * (1.0 - 4.0 * w);
|
||||
}
|
||||
Assert.Equal(sumAdjH / 5.0, upper.Last.Value, 1e-10);
|
||||
Assert.Equal(sumAdjL / 5.0, lower.Last.Value, 1e-10);
|
||||
|
||||
_output.WriteLine("AccBands manual calculation (period 5) validated successfully");
|
||||
}
|
||||
@@ -115,31 +127,28 @@ public sealed class AccBandsValidationTests : IDisposable
|
||||
series.Add(new TBar(time.AddMinutes(i), 100, 110, 90, 100, 1000));
|
||||
}
|
||||
|
||||
// With constant H/L/C: SMA(High)=110, SMA(Low)=90, SMA(Close)=100
|
||||
// Spread = 110 - 90 = 20
|
||||
// With constant H=110,L=90: w = 20/200 = 0.1 per bar
|
||||
|
||||
var (middle1, upper1, lower1) = AccBands.Batch(series, 5, 1.0);
|
||||
var (middle2, upper2, lower2) = AccBands.Batch(series, 5, 2.0);
|
||||
var (middle3, upper3, lower3) = AccBands.Batch(series, 5, 3.0);
|
||||
var (middle1, upper1, lower1) = AccBands.Batch(series, 5, 2.0);
|
||||
var (middle2, upper2, lower2) = AccBands.Batch(series, 5, 4.0);
|
||||
var (middle3, upper3, lower3) = AccBands.Batch(series, 5, 6.0);
|
||||
|
||||
// Middle should be the same regardless of factor
|
||||
// Middle should be the same regardless of factor (SMA of Close = 100)
|
||||
Assert.Equal(middle1.Last.Value, middle2.Last.Value, 1e-10);
|
||||
Assert.Equal(middle2.Last.Value, middle3.Last.Value, 1e-10);
|
||||
Assert.Equal(100.0, middle1.Last.Value, 1e-10);
|
||||
|
||||
// BandWidth with factor 1.0 = 20
|
||||
// BandWidth with factor 2.0 = 40
|
||||
// BandWidth with factor 3.0 = 60
|
||||
// factor=2: adjH=110*(1+2*0.1)=110*1.2=132, adjL=90*(1-2*0.1)=90*0.8=72
|
||||
// factor=4: adjH=110*(1+4*0.1)=110*1.4=154, adjL=90*(1-4*0.1)=90*0.6=54
|
||||
// factor=6: adjH=110*(1+6*0.1)=110*1.6=176, adjL=90*(1-6*0.1)=90*0.4=36
|
||||
|
||||
// Upper = SMA(High) + BandWidth
|
||||
Assert.Equal(110.0 + 20.0, upper1.Last.Value, 1e-10); // 130
|
||||
Assert.Equal(110.0 + 40.0, upper2.Last.Value, 1e-10); // 150
|
||||
Assert.Equal(110.0 + 60.0, upper3.Last.Value, 1e-10); // 170
|
||||
Assert.Equal(132.0, upper1.Last.Value, 1e-10);
|
||||
Assert.Equal(154.0, upper2.Last.Value, 1e-10);
|
||||
Assert.Equal(176.0, upper3.Last.Value, 1e-10);
|
||||
|
||||
// Lower = SMA(Low) - BandWidth
|
||||
Assert.Equal(90.0 - 20.0, lower1.Last.Value, 1e-10); // 70
|
||||
Assert.Equal(90.0 - 40.0, lower2.Last.Value, 1e-10); // 50
|
||||
Assert.Equal(90.0 - 60.0, lower3.Last.Value, 1e-10); // 30
|
||||
Assert.Equal(72.0, lower1.Last.Value, 1e-10);
|
||||
Assert.Equal(54.0, lower2.Last.Value, 1e-10);
|
||||
Assert.Equal(36.0, lower3.Last.Value, 1e-10);
|
||||
|
||||
_output.WriteLine("AccBands factor effect validated successfully");
|
||||
}
|
||||
@@ -152,11 +161,11 @@ public sealed class AccBandsValidationTests : IDisposable
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Batch mode using instance
|
||||
var accBands = new AccBands(period, 2.0);
|
||||
var accBands = new AccBands(period, 4.0);
|
||||
var (qMiddle, qUpper, qLower) = accBands.Update(_testData.Bars);
|
||||
|
||||
// Static batch
|
||||
var (sMiddle, sUpper, sLower) = AccBands.Batch(_testData.Bars, period, 2.0);
|
||||
var (sMiddle, sUpper, sLower) = AccBands.Batch(_testData.Bars, period, 4.0);
|
||||
|
||||
// Verify match
|
||||
ValidationHelper.VerifySeriesEqual(qMiddle, sMiddle);
|
||||
@@ -174,7 +183,7 @@ public sealed class AccBandsValidationTests : IDisposable
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Streaming mode
|
||||
var streamingAcc = new AccBands(period, 2.0);
|
||||
var streamingAcc = new AccBands(period, 4.0);
|
||||
var streamMiddle = new TSeries();
|
||||
var streamUpper = new TSeries();
|
||||
var streamLower = new TSeries();
|
||||
@@ -187,7 +196,7 @@ public sealed class AccBandsValidationTests : IDisposable
|
||||
}
|
||||
|
||||
// Batch mode for comparison
|
||||
var (batchMiddle, batchUpper, batchLower) = AccBands.Batch(_testData.Bars, period, 2.0);
|
||||
var (batchMiddle, batchUpper, batchLower) = AccBands.Batch(_testData.Bars, period, 4.0);
|
||||
|
||||
// Verify match
|
||||
ValidationHelper.VerifySeriesEqual(batchMiddle, streamMiddle);
|
||||
@@ -216,10 +225,10 @@ public sealed class AccBandsValidationTests : IDisposable
|
||||
|
||||
AccBands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(),
|
||||
spanMiddle.AsSpan(), spanUpper.AsSpan(), spanLower.AsSpan(),
|
||||
period, 2.0);
|
||||
period, 4.0);
|
||||
|
||||
// Batch mode for comparison
|
||||
var (batchMiddle, batchUpper, batchLower) = AccBands.Batch(_testData.Bars, period, 2.0);
|
||||
var (batchMiddle, batchUpper, batchLower) = AccBands.Batch(_testData.Bars, period, 4.0);
|
||||
|
||||
// Verify match
|
||||
for (int i = 0; i < len; i++)
|
||||
@@ -241,7 +250,7 @@ public sealed class AccBandsValidationTests : IDisposable
|
||||
{
|
||||
// Eventing mode
|
||||
var pubSource = new TBarSeries();
|
||||
var eventingInd = new AccBands(pubSource, period, 2.0);
|
||||
var eventingInd = new AccBands(pubSource, period, 4.0);
|
||||
var eventMiddle = new TSeries();
|
||||
var eventUpper = new TSeries();
|
||||
var eventLower = new TSeries();
|
||||
@@ -255,7 +264,7 @@ public sealed class AccBandsValidationTests : IDisposable
|
||||
}
|
||||
|
||||
// Batch mode for comparison
|
||||
var (batchMiddle, batchUpper, batchLower) = AccBands.Batch(_testData.Bars, period, 2.0);
|
||||
var (batchMiddle, batchUpper, batchLower) = AccBands.Batch(_testData.Bars, period, 4.0);
|
||||
|
||||
// Verify match
|
||||
ValidationHelper.VerifySeriesEqual(batchMiddle, eventMiddle);
|
||||
@@ -272,7 +281,7 @@ public sealed class AccBandsValidationTests : IDisposable
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var ((middle, upper, lower), indicator) = AccBands.Calculate(_testData.Bars, period, 2.0);
|
||||
var ((middle, upper, lower), indicator) = AccBands.Calculate(_testData.Bars, period, 4.0);
|
||||
|
||||
// Verify indicator is hot
|
||||
Assert.True(indicator.IsHot);
|
||||
@@ -295,7 +304,7 @@ public sealed class AccBandsValidationTests : IDisposable
|
||||
public void Validate_LargeDataset_NoOverflow()
|
||||
{
|
||||
// Test with the full 5000 bar dataset
|
||||
var (middle, upper, lower) = AccBands.Batch(_testData.Bars, 100, 2.0);
|
||||
var (middle, upper, lower) = AccBands.Batch(_testData.Bars, 100, 4.0);
|
||||
|
||||
// All outputs should be finite
|
||||
ValidationHelper.VerifyAllFinite(middle, startIndex: 0);
|
||||
@@ -314,41 +323,6 @@ public sealed class AccBandsValidationTests : IDisposable
|
||||
_output.WriteLine("AccBands large dataset (5000 bars) validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_BandWidth_IsSymmetric()
|
||||
{
|
||||
// Verify that Upper - SMA(High) == SMA(Low) - Lower
|
||||
// This confirms the band width is applied symmetrically
|
||||
|
||||
var (middle, upper, lower) = AccBands.Batch(_testData.Bars, 20, 2.0);
|
||||
|
||||
// Calculate SMA(High) and SMA(Low) separately for verification
|
||||
_ = middle; // Suppress unused variable warning - middle is not needed for symmetry test
|
||||
var smaHigh = new Sma(20);
|
||||
var smaLow = new Sma(20);
|
||||
|
||||
var smaHighResults = new TSeries();
|
||||
var smaLowResults = new TSeries();
|
||||
|
||||
for (int i = 0; i < _testData.Bars.Count; i++)
|
||||
{
|
||||
var bar = _testData.Bars[i];
|
||||
smaHighResults.Add(smaHigh.Update(new TValue(bar.Time, bar.High)));
|
||||
smaLowResults.Add(smaLow.Update(new TValue(bar.Time, bar.Low)));
|
||||
}
|
||||
|
||||
// After warmup, verify symmetry
|
||||
for (int i = 20; i < _testData.Bars.Count; i++)
|
||||
{
|
||||
double upperDiff = upper[i].Value - smaHighResults[i].Value;
|
||||
double lowerDiff = smaLowResults[i].Value - lower[i].Value;
|
||||
|
||||
Assert.Equal(upperDiff, lowerDiff, 1e-9);
|
||||
}
|
||||
|
||||
_output.WriteLine("AccBands band width symmetry validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Prime_ProducesCorrectState()
|
||||
{
|
||||
@@ -356,10 +330,10 @@ public sealed class AccBandsValidationTests : IDisposable
|
||||
const int period = 20;
|
||||
|
||||
// Full batch calculation
|
||||
var (batchMiddle, batchUpper, batchLower) = AccBands.Batch(_testData.Bars, period, 2.0);
|
||||
var (batchMiddle, batchUpper, batchLower) = AccBands.Batch(_testData.Bars, period, 4.0);
|
||||
|
||||
// Prime indicator with subset and continue
|
||||
var primedIndicator = new AccBands(period, 2.0);
|
||||
var primedIndicator = new AccBands(period, 4.0);
|
||||
var subset = new TBarSeries();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
@@ -382,10 +356,13 @@ public sealed class AccBandsValidationTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Talib_MiddleBand_Batch()
|
||||
public void Validate_Talib_AllBands_Batch()
|
||||
{
|
||||
// TALib ACCBANDS uses a different upper/lower formula (per-bar adaptive width via
|
||||
// High*(1+4*(H-L)/(H+L))) but the MIDDLE band is SMA(Close) which matches exactly.
|
||||
// TA-Lib ACCBANDS uses the same Headley formula:
|
||||
// Upper = SMA(High*(1+4*(H-L)/(H+L)), period)
|
||||
// Lower = SMA(Low*(1-4*(H-L)/(H+L)), period)
|
||||
// Middle = SMA(Close, period)
|
||||
// Now all three bands should match exactly.
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
double[] high = _testData.HighPrices.ToArray();
|
||||
@@ -399,8 +376,8 @@ public sealed class AccBandsValidationTests : IDisposable
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// QuanTAlib AccBands (batch)
|
||||
var (qMiddle, _, _) = AccBands.Batch(_testData.Bars, period, 2.0);
|
||||
// QuanTAlib AccBands (batch) with factor=4 to match TA-Lib default
|
||||
var (qMiddle, qUpper, qLower) = AccBands.Batch(_testData.Bars, period, 4.0);
|
||||
|
||||
// TALib Accbands
|
||||
var retCode = Functions.Accbands<double>(
|
||||
@@ -414,16 +391,18 @@ public sealed class AccBandsValidationTests : IDisposable
|
||||
|
||||
int lookback = Functions.AccbandsLookback(period);
|
||||
|
||||
// Middle band = SMA(Close) in both implementations — should match exactly
|
||||
// All three bands should match (same Headley formula)
|
||||
ValidationHelper.VerifyData(qMiddle, talibMiddle, outRange, lookback);
|
||||
ValidationHelper.VerifyData(qUpper, talibUpper, outRange, lookback);
|
||||
ValidationHelper.VerifyData(qLower, talibLower, outRange, lookback);
|
||||
}
|
||||
_output.WriteLine("AccBands middle band validated successfully against TA-Lib");
|
||||
_output.WriteLine("AccBands all bands validated successfully against TA-Lib");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Talib_MiddleBand_Span()
|
||||
public void Validate_Talib_AllBands_Span()
|
||||
{
|
||||
// Validate middle band match using Span API
|
||||
// Validate all band match using Span API
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
double[] high = _testData.HighPrices.ToArray();
|
||||
@@ -437,13 +416,13 @@ public sealed class AccBandsValidationTests : IDisposable
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// QuanTAlib AccBands (Span API)
|
||||
// QuanTAlib AccBands (Span API) with factor=4
|
||||
double[] qMiddle = new double[len];
|
||||
double[] qUpper = new double[len];
|
||||
double[] qLower = new double[len];
|
||||
AccBands.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(),
|
||||
qMiddle.AsSpan(), qUpper.AsSpan(), qLower.AsSpan(),
|
||||
period, 2.0);
|
||||
period, 4.0);
|
||||
|
||||
// TALib Accbands
|
||||
var retCode = Functions.Accbands<double>(
|
||||
@@ -457,20 +436,18 @@ public sealed class AccBandsValidationTests : IDisposable
|
||||
|
||||
int lookback = Functions.AccbandsLookback(period);
|
||||
|
||||
// Middle band = SMA(Close) — exact match
|
||||
// All three bands should match
|
||||
ValidationHelper.VerifyData(qMiddle, talibMiddle, outRange, lookback);
|
||||
ValidationHelper.VerifyData(qUpper, talibUpper, outRange, lookback);
|
||||
ValidationHelper.VerifyData(qLower, talibLower, outRange, lookback);
|
||||
}
|
||||
_output.WriteLine("AccBands Span middle band validated successfully against TA-Lib");
|
||||
_output.WriteLine("AccBands Span all bands validated successfully against TA-Lib");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Talib_FormulaConventionDifference()
|
||||
public void Validate_Talib_StructuralRelationships()
|
||||
{
|
||||
// Document and verify that upper/lower bands differ between implementations.
|
||||
// TALib: Upper = SMA(High * (1 + 4*(H-L)/(H+L))), per-bar adaptive width
|
||||
// QuanTAlib: Upper = SMA(High) + factor*(SMA(High)-SMA(Low)), SMA-based width
|
||||
// Both are valid "Acceleration Bands" variants.
|
||||
|
||||
// Verify structural relationships hold for both implementations
|
||||
const int period = 20;
|
||||
|
||||
double[] high = _testData.HighPrices.ToArray();
|
||||
@@ -491,29 +468,18 @@ public sealed class AccBandsValidationTests : IDisposable
|
||||
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
|
||||
var (qMiddle, qUpper, qLower) = AccBands.Batch(_testData.Bars, period, 2.0);
|
||||
var (qMiddle, qUpper, qLower) = AccBands.Batch(_testData.Bars, period, 4.0);
|
||||
|
||||
int lookback = Functions.AccbandsLookback(period);
|
||||
int talibStart = outRange.Start.Value;
|
||||
|
||||
// Middle bands should match (both SMA of Close)
|
||||
for (int i = lookback; i < qMiddle.Count && (i - talibStart) < len; i++)
|
||||
{
|
||||
int tIdx = i - talibStart;
|
||||
if (tIdx >= 0 && tIdx < len && talibMiddle[tIdx] != 0)
|
||||
{
|
||||
Assert.Equal(qMiddle[i].Value, talibMiddle[tIdx], 1e-7);
|
||||
}
|
||||
}
|
||||
|
||||
// Upper/Lower bands should differ (different formulas) but maintain same structure
|
||||
// Both should have Upper > Middle > Lower
|
||||
int structuralCount = 0;
|
||||
for (int i = lookback; i < qMiddle.Count && (i - talibStart) < len; i++)
|
||||
{
|
||||
int tIdx = i - talibStart;
|
||||
if (tIdx >= 0 && tIdx < len && talibUpper[tIdx] != 0)
|
||||
{
|
||||
// Both should have Upper > Middle > Lower
|
||||
Assert.True(qUpper[i].Value > qMiddle[i].Value, $"Q: Upper > Middle at {i}");
|
||||
Assert.True(qLower[i].Value < qMiddle[i].Value, $"Q: Lower < Middle at {i}");
|
||||
Assert.True(talibUpper[tIdx] > talibMiddle[tIdx], $"TALib: Upper > Middle at {i}");
|
||||
@@ -523,6 +489,6 @@ public sealed class AccBandsValidationTests : IDisposable
|
||||
}
|
||||
|
||||
Assert.True(structuralCount > 100, $"Validated {structuralCount} bars structurally");
|
||||
_output.WriteLine($"AccBands formula convention difference validated ({structuralCount} bars)");
|
||||
_output.WriteLine($"AccBands structural relationships validated ({structuralCount} bars)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,30 +9,30 @@ namespace QuanTAlib;
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Acceleration Bands are a volatility-based channel indicator developed by Price Headley.
|
||||
/// They create an adaptive price envelope around a moving average, with band width determined
|
||||
/// by the spread between the high and low moving averages multiplied by a factor.
|
||||
/// They create an adaptive price envelope around a moving average, where the band width
|
||||
/// is determined by the per-bar normalized range applied before averaging.
|
||||
///
|
||||
/// Calculation:
|
||||
/// Calculation (Headley's original formula):
|
||||
/// w = (High - Low) / (High + Low) // normalized range width per bar
|
||||
/// Upper Band = SMA(High × (1 + factor × w), Period)
|
||||
/// Lower Band = SMA(Low × (1 - factor × w), Period)
|
||||
/// Middle Band = SMA(Close, Period)
|
||||
/// BandWidth = [SMA(High, Period) - SMA(Low, Period)] × Factor
|
||||
/// Upper Band = SMA(High, Period) + BandWidth
|
||||
/// Lower Band = SMA(Low, Period) - BandWidth
|
||||
///
|
||||
/// Key characteristics:
|
||||
/// - Width adjustment is applied per bar before averaging (Headley's method)
|
||||
/// - Bands expand during volatile periods and contract during consolidation
|
||||
/// - Uses SMA of High, Low, and Close for calculations
|
||||
/// - Factor parameter controls band sensitivity
|
||||
/// - Factor parameter (default 4.0) controls band sensitivity
|
||||
///
|
||||
/// Sources:
|
||||
/// Headley, P. (2002). Big Trends in Trading. John Wiley & Sons.
|
||||
/// Headley, P. (2002). Big Trends in Trading. John Wiley & Sons.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class AccBands : ITValuePublisher, IDisposable
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly double _factor;
|
||||
private readonly RingBuffer _highBuffer;
|
||||
private readonly RingBuffer _lowBuffer;
|
||||
private readonly RingBuffer _adjHighBuffer;
|
||||
private readonly RingBuffer _adjLowBuffer;
|
||||
private readonly RingBuffer _closeBuffer;
|
||||
private readonly TBarPublishedHandler _barHandler;
|
||||
private TBarSeries? _source;
|
||||
@@ -42,8 +42,8 @@ public sealed class AccBands : ITValuePublisher, IDisposable
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
double SumHigh,
|
||||
double SumLow,
|
||||
double SumAdjHigh,
|
||||
double SumAdjLow,
|
||||
double SumClose,
|
||||
double LastValidHigh,
|
||||
double LastValidLow,
|
||||
@@ -92,8 +92,8 @@ public sealed class AccBands : ITValuePublisher, IDisposable
|
||||
/// Creates AccBands with specified period and factor.
|
||||
/// </summary>
|
||||
/// <param name="period">Lookback period for SMA calculations (must be > 0)</param>
|
||||
/// <param name="factor">Multiplier for band width (must be > 0, default: 2.0)</param>
|
||||
public AccBands(int period, double factor = 2.0)
|
||||
/// <param name="factor">Multiplier for normalized width (must be > 0, default: 4.0 per Headley)</param>
|
||||
public AccBands(int period, double factor = 4.0)
|
||||
{
|
||||
if (period <= 0)
|
||||
{
|
||||
@@ -107,8 +107,8 @@ public sealed class AccBands : ITValuePublisher, IDisposable
|
||||
|
||||
_period = period;
|
||||
_factor = factor;
|
||||
_highBuffer = new RingBuffer(period);
|
||||
_lowBuffer = new RingBuffer(period);
|
||||
_adjHighBuffer = new RingBuffer(period);
|
||||
_adjLowBuffer = new RingBuffer(period);
|
||||
_closeBuffer = new RingBuffer(period);
|
||||
Name = $"AccBands({period},{factor:F2})";
|
||||
WarmupPeriod = period;
|
||||
@@ -118,7 +118,7 @@ public sealed class AccBands : ITValuePublisher, IDisposable
|
||||
/// <summary>
|
||||
/// Creates AccBands with TBarSeries source.
|
||||
/// </summary>
|
||||
public AccBands(TBarSeries source, int period, double factor = 2.0) : this(period, factor)
|
||||
public AccBands(TBarSeries source, int period, double factor = 4.0) : this(period, factor)
|
||||
{
|
||||
_source = source;
|
||||
Prime(source);
|
||||
@@ -191,27 +191,36 @@ public sealed class AccBands : ITValuePublisher, IDisposable
|
||||
return _state.LastValidClose;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes Headley's per-bar adjusted values and updates running sums.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void UpdateState(double high, double low, double close)
|
||||
{
|
||||
double removedHigh = _highBuffer.Count == _highBuffer.Capacity ? _highBuffer.Oldest : 0.0;
|
||||
double removedLow = _lowBuffer.Count == _lowBuffer.Capacity ? _lowBuffer.Oldest : 0.0;
|
||||
// Headley's per-bar normalized width
|
||||
double denom = high + low;
|
||||
double w = denom != 0.0 ? (high - low) / denom : 0.0;
|
||||
double adjHigh = high * (1.0 + _factor * w);
|
||||
double adjLow = low * (1.0 - _factor * w);
|
||||
|
||||
double removedAdjHigh = _adjHighBuffer.Count == _adjHighBuffer.Capacity ? _adjHighBuffer.Oldest : 0.0;
|
||||
double removedAdjLow = _adjLowBuffer.Count == _adjLowBuffer.Capacity ? _adjLowBuffer.Oldest : 0.0;
|
||||
double removedClose = _closeBuffer.Count == _closeBuffer.Capacity ? _closeBuffer.Oldest : 0.0;
|
||||
|
||||
_state.SumHigh = _state.SumHigh - removedHigh + high;
|
||||
_state.SumLow = _state.SumLow - removedLow + low;
|
||||
_state.SumAdjHigh = _state.SumAdjHigh - removedAdjHigh + adjHigh;
|
||||
_state.SumAdjLow = _state.SumAdjLow - removedAdjLow + adjLow;
|
||||
_state.SumClose = _state.SumClose - removedClose + close;
|
||||
|
||||
_highBuffer.Add(high);
|
||||
_lowBuffer.Add(low);
|
||||
_adjHighBuffer.Add(adjHigh);
|
||||
_adjLowBuffer.Add(adjLow);
|
||||
_closeBuffer.Add(close);
|
||||
|
||||
_state.TickCount++;
|
||||
if (_closeBuffer.IsFull && _state.TickCount >= ResyncInterval)
|
||||
{
|
||||
_state.TickCount = 0;
|
||||
_state.SumHigh = _highBuffer.RecalculateSum();
|
||||
_state.SumLow = _lowBuffer.RecalculateSum();
|
||||
_state.SumAdjHigh = _adjHighBuffer.RecalculateSum();
|
||||
_state.SumAdjLow = _adjLowBuffer.RecalculateSum();
|
||||
_state.SumClose = _closeBuffer.RecalculateSum();
|
||||
}
|
||||
}
|
||||
@@ -239,14 +248,20 @@ public sealed class AccBands : ITValuePublisher, IDisposable
|
||||
double low = GetValidLow(input.Low);
|
||||
double close = GetValidClose(input.Close);
|
||||
|
||||
_highBuffer.UpdateNewest(high);
|
||||
_lowBuffer.UpdateNewest(low);
|
||||
// Recompute adjusted values for the corrected bar
|
||||
double denom = high + low;
|
||||
double w = denom != 0.0 ? (high - low) / denom : 0.0;
|
||||
double adjHigh = high * (1.0 + _factor * w);
|
||||
double adjLow = low * (1.0 - _factor * w);
|
||||
|
||||
_adjHighBuffer.UpdateNewest(adjHigh);
|
||||
_adjLowBuffer.UpdateNewest(adjLow);
|
||||
_closeBuffer.UpdateNewest(close);
|
||||
|
||||
_state = _state with
|
||||
{
|
||||
SumHigh = _highBuffer.Sum,
|
||||
SumLow = _lowBuffer.Sum,
|
||||
SumAdjHigh = _adjHighBuffer.Sum,
|
||||
SumAdjLow = _adjLowBuffer.Sum,
|
||||
SumClose = _closeBuffer.Sum,
|
||||
};
|
||||
}
|
||||
@@ -260,14 +275,13 @@ public sealed class AccBands : ITValuePublisher, IDisposable
|
||||
}
|
||||
else
|
||||
{
|
||||
double smaHigh = _state.SumHigh / count;
|
||||
double smaLow = _state.SumLow / count;
|
||||
double smaAdjHigh = _state.SumAdjHigh / count;
|
||||
double smaAdjLow = _state.SumAdjLow / count;
|
||||
double smaClose = _state.SumClose / count;
|
||||
double bandWidth = (smaHigh - smaLow) * _factor;
|
||||
|
||||
Last = new TValue(input.Time, smaClose);
|
||||
Upper = new TValue(input.Time, smaHigh + bandWidth);
|
||||
Lower = new TValue(input.Time, smaLow - bandWidth);
|
||||
Upper = new TValue(input.Time, smaAdjHigh);
|
||||
Lower = new TValue(input.Time, smaAdjLow);
|
||||
}
|
||||
|
||||
PubEvent(Last, isNew);
|
||||
@@ -332,8 +346,8 @@ public sealed class AccBands : ITValuePublisher, IDisposable
|
||||
}
|
||||
|
||||
// Reset state
|
||||
_highBuffer.Clear();
|
||||
_lowBuffer.Clear();
|
||||
_adjHighBuffer.Clear();
|
||||
_adjLowBuffer.Clear();
|
||||
_closeBuffer.Clear();
|
||||
_state = default;
|
||||
_p_state = default;
|
||||
@@ -413,14 +427,13 @@ public sealed class AccBands : ITValuePublisher, IDisposable
|
||||
if (count > 0)
|
||||
{
|
||||
var lastBar = source.Last;
|
||||
double smaHigh = _state.SumHigh / count;
|
||||
double smaLow = _state.SumLow / count;
|
||||
double smaAdjHigh = _state.SumAdjHigh / count;
|
||||
double smaAdjLow = _state.SumAdjLow / count;
|
||||
double smaClose = _state.SumClose / count;
|
||||
double bandWidth = (smaHigh - smaLow) * _factor;
|
||||
|
||||
Last = new TValue(lastBar.Time, smaClose);
|
||||
Upper = new TValue(lastBar.Time, smaHigh + bandWidth);
|
||||
Lower = new TValue(lastBar.Time, smaLow - bandWidth);
|
||||
Upper = new TValue(lastBar.Time, smaAdjHigh);
|
||||
Lower = new TValue(lastBar.Time, smaAdjLow);
|
||||
}
|
||||
|
||||
_p_state = _state;
|
||||
@@ -431,12 +444,12 @@ public sealed class AccBands : ITValuePublisher, IDisposable
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
_highBuffer.Clear();
|
||||
_lowBuffer.Clear();
|
||||
_adjHighBuffer.Clear();
|
||||
_adjLowBuffer.Clear();
|
||||
_closeBuffer.Clear();
|
||||
_state = new State(
|
||||
SumHigh: 0,
|
||||
SumLow: 0,
|
||||
SumAdjHigh: 0,
|
||||
SumAdjLow: 0,
|
||||
SumClose: 0,
|
||||
LastValidHigh: double.NaN,
|
||||
LastValidLow: double.NaN,
|
||||
@@ -515,8 +528,8 @@ public sealed class AccBands : ITValuePublisher, IDisposable
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private ref struct ScalarState
|
||||
{
|
||||
public double SumHigh;
|
||||
public double SumLow;
|
||||
public double SumAdjHigh;
|
||||
public double SumAdjLow;
|
||||
public double SumClose;
|
||||
public double LastValidHigh;
|
||||
public double LastValidLow;
|
||||
@@ -529,17 +542,17 @@ public sealed class AccBands : ITValuePublisher, IDisposable
|
||||
/// Working buffers for batch calculation.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private readonly ref struct WorkBuffers(Span<double> high, Span<double> low, Span<double> close)
|
||||
private readonly ref struct WorkBuffers(Span<double> adjHigh, Span<double> adjLow, Span<double> close)
|
||||
{
|
||||
public readonly Span<double> High = high;
|
||||
public readonly Span<double> Low = low;
|
||||
public readonly Span<double> AdjHigh = adjHigh;
|
||||
public readonly Span<double> AdjLow = adjLow;
|
||||
public readonly Span<double> Close = close;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates AccBands for the entire TBarSeries using a new instance.
|
||||
/// </summary>
|
||||
public static (TSeries Middle, TSeries Upper, TSeries Lower) Batch(TBarSeries source, int period, double factor = 2.0)
|
||||
public static (TSeries Middle, TSeries Upper, TSeries Lower) Batch(TBarSeries source, int period, double factor = 4.0)
|
||||
{
|
||||
var accBands = new AccBands(period, factor);
|
||||
return accBands.Update(source);
|
||||
@@ -558,7 +571,7 @@ public sealed class AccBands : ITValuePublisher, IDisposable
|
||||
BatchInputs inputs,
|
||||
BatchOutputs outputs,
|
||||
int period,
|
||||
double factor = 2.0)
|
||||
double factor = 4.0)
|
||||
{
|
||||
Batch(inputs.High, inputs.Low, inputs.Close, outputs.Middle, outputs.Upper, outputs.Lower, period, factor);
|
||||
}
|
||||
@@ -580,7 +593,7 @@ public sealed class AccBands : ITValuePublisher, IDisposable
|
||||
ReadOnlySpan<double> close,
|
||||
BatchOutputs outputs,
|
||||
int period,
|
||||
double factor = 2.0)
|
||||
double factor = 4.0)
|
||||
{
|
||||
Batch(high, low, close, outputs.Middle, outputs.Upper, outputs.Lower, period, factor);
|
||||
}
|
||||
@@ -609,7 +622,7 @@ public sealed class AccBands : ITValuePublisher, IDisposable
|
||||
Span<double> upper,
|
||||
Span<double> lower,
|
||||
int period,
|
||||
double factor = 2.0)
|
||||
double factor = 4.0)
|
||||
#pragma warning restore S107
|
||||
{
|
||||
int len = close.Length;
|
||||
@@ -654,15 +667,15 @@ public sealed class AccBands : ITValuePublisher, IDisposable
|
||||
int len = inputs.Close.Length;
|
||||
|
||||
// Always use ArrayPool to avoid span scope safety issues with stackalloc + ref structs
|
||||
double[] rentedHigh = ArrayPool<double>.Shared.Rent(period);
|
||||
double[] rentedLow = ArrayPool<double>.Shared.Rent(period);
|
||||
double[] rentedAdjHigh = ArrayPool<double>.Shared.Rent(period);
|
||||
double[] rentedAdjLow = ArrayPool<double>.Shared.Rent(period);
|
||||
double[] rentedClose = ArrayPool<double>.Shared.Rent(period);
|
||||
|
||||
try
|
||||
{
|
||||
var buffers = new WorkBuffers(
|
||||
rentedHigh.AsSpan(0, period),
|
||||
rentedLow.AsSpan(0, period),
|
||||
rentedAdjHigh.AsSpan(0, period),
|
||||
rentedAdjLow.AsSpan(0, period),
|
||||
rentedClose.AsSpan(0, period));
|
||||
|
||||
var state = new ScalarState
|
||||
@@ -680,8 +693,8 @@ public sealed class AccBands : ITValuePublisher, IDisposable
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<double>.Shared.Return(rentedHigh);
|
||||
ArrayPool<double>.Shared.Return(rentedLow);
|
||||
ArrayPool<double>.Shared.Return(rentedAdjHigh);
|
||||
ArrayPool<double>.Shared.Return(rentedAdjLow);
|
||||
ArrayPool<double>.Shared.Return(rentedClose);
|
||||
}
|
||||
}
|
||||
@@ -751,13 +764,15 @@ public sealed class AccBands : ITValuePublisher, IDisposable
|
||||
return (h, l, c);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes adjusted high/low per bar using Headley's formula and writes band outputs.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteBandOutputs(scoped BatchOutputs outputs, int i, double smaHigh, double smaLow, double smaClose, double factor)
|
||||
private static void WriteBandOutputs(scoped BatchOutputs outputs, int i, double smaAdjHigh, double smaAdjLow, double smaClose)
|
||||
{
|
||||
double bandWidth = (smaHigh - smaLow) * factor;
|
||||
outputs.Middle[i] = smaClose;
|
||||
outputs.Upper[i] = smaHigh + bandWidth;
|
||||
outputs.Lower[i] = smaLow - bandWidth;
|
||||
outputs.Upper[i] = smaAdjHigh;
|
||||
outputs.Lower[i] = smaAdjLow;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
@@ -773,16 +788,22 @@ public sealed class AccBands : ITValuePublisher, IDisposable
|
||||
{
|
||||
var (h, l, c) = GetValidHLC(inputs, i, ref state);
|
||||
|
||||
state.SumHigh += h;
|
||||
state.SumLow += l;
|
||||
// Headley's per-bar adjustment
|
||||
double denom = h + l;
|
||||
double w = denom != 0.0 ? (h - l) / denom : 0.0;
|
||||
double adjHigh = h * (1.0 + factor * w);
|
||||
double adjLow = l * (1.0 - factor * w);
|
||||
|
||||
state.SumAdjHigh += adjHigh;
|
||||
state.SumAdjLow += adjLow;
|
||||
state.SumClose += c;
|
||||
|
||||
buffers.High[i] = h;
|
||||
buffers.Low[i] = l;
|
||||
buffers.AdjHigh[i] = adjHigh;
|
||||
buffers.AdjLow[i] = adjLow;
|
||||
buffers.Close[i] = c;
|
||||
|
||||
int count = i + 1;
|
||||
WriteBandOutputs(outputs, i, state.SumHigh / count, state.SumLow / count, state.SumClose / count, factor);
|
||||
WriteBandOutputs(outputs, i, state.SumAdjHigh / count, state.SumAdjLow / count, state.SumClose / count);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -801,12 +822,18 @@ public sealed class AccBands : ITValuePublisher, IDisposable
|
||||
{
|
||||
var (h, l, c) = GetValidHLC(inputs, i, ref state);
|
||||
|
||||
state.SumHigh = state.SumHigh - buffers.High[state.BufferIndex] + h;
|
||||
state.SumLow = state.SumLow - buffers.Low[state.BufferIndex] + l;
|
||||
// Headley's per-bar adjustment
|
||||
double denom = h + l;
|
||||
double w = denom != 0.0 ? (h - l) / denom : 0.0;
|
||||
double adjHigh = h * (1.0 + factor * w);
|
||||
double adjLow = l * (1.0 - factor * w);
|
||||
|
||||
state.SumAdjHigh = state.SumAdjHigh - buffers.AdjHigh[state.BufferIndex] + adjHigh;
|
||||
state.SumAdjLow = state.SumAdjLow - buffers.AdjLow[state.BufferIndex] + adjLow;
|
||||
state.SumClose = state.SumClose - buffers.Close[state.BufferIndex] + c;
|
||||
|
||||
buffers.High[state.BufferIndex] = h;
|
||||
buffers.Low[state.BufferIndex] = l;
|
||||
buffers.AdjHigh[state.BufferIndex] = adjHigh;
|
||||
buffers.AdjLow[state.BufferIndex] = adjLow;
|
||||
buffers.Close[state.BufferIndex] = c;
|
||||
|
||||
state.BufferIndex++;
|
||||
@@ -815,7 +842,7 @@ public sealed class AccBands : ITValuePublisher, IDisposable
|
||||
state.BufferIndex = 0;
|
||||
}
|
||||
|
||||
WriteBandOutputs(outputs, i, state.SumHigh / period, state.SumLow / period, state.SumClose / period, factor);
|
||||
WriteBandOutputs(outputs, i, state.SumAdjHigh / period, state.SumAdjLow / period, state.SumClose / period);
|
||||
|
||||
state.TickCount++;
|
||||
if (state.TickCount >= ResyncInterval)
|
||||
@@ -829,18 +856,18 @@ public sealed class AccBands : ITValuePublisher, IDisposable
|
||||
private static void ResyncSums(int period, ref WorkBuffers buffers, ref ScalarState state)
|
||||
{
|
||||
state.TickCount = 0;
|
||||
ReadOnlySpan<double> highSpan = buffers.High[..period];
|
||||
ReadOnlySpan<double> lowSpan = buffers.Low[..period];
|
||||
ReadOnlySpan<double> adjHighSpan = buffers.AdjHigh[..period];
|
||||
ReadOnlySpan<double> adjLowSpan = buffers.AdjLow[..period];
|
||||
ReadOnlySpan<double> closeSpan = buffers.Close[..period];
|
||||
state.SumHigh = highSpan.SumSIMD();
|
||||
state.SumLow = lowSpan.SumSIMD();
|
||||
state.SumAdjHigh = adjHighSpan.SumSIMD();
|
||||
state.SumAdjLow = adjLowSpan.SumSIMD();
|
||||
state.SumClose = closeSpan.SumSIMD();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a high-performance batch calculation and returns a "Hot" AccBands instance.
|
||||
/// </summary>
|
||||
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, AccBands Indicator) Calculate(TBarSeries source, int period, double factor = 2.0)
|
||||
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, AccBands Indicator) Calculate(TBarSeries source, int period, double factor = 4.0)
|
||||
{
|
||||
var accBands = new AccBands(period, factor);
|
||||
var results = accBands.Update(source);
|
||||
|
||||
@@ -6,45 +6,44 @@ Acceleration Bands (ACCBANDS) serve as an adaptive volatility envelope based on
|
||||
|
||||
## Historical Context
|
||||
|
||||
Developed by Price Headley and detailed in *Big Trends in Trading* (2002), Acceleration Bands addressed the need for a breakout-specific envelope. Headley observed that standard deviation often lagged in fast-moving breakout scenarios. By incorporating the High and Low prices directly into the band width calculation, he created a system that reacts immediately to range expansion, often serving as a trigger for trend-following entries when price closes outside the bands.
|
||||
Developed by Price Headley and detailed in *Big Trends in Trading* (2002), Acceleration Bands addressed the need for a breakout-specific envelope. Headley observed that standard deviation often lagged in fast-moving breakout scenarios. By incorporating the High and Low prices directly into the band width calculation — using a per-bar normalized range width — he created a system that reacts immediately to range expansion, often serving as a trigger for trend-following entries when price closes outside the bands.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
The indicator maintains three parallel Simple Moving Averages (High, Low, and Close) to construct the bands. The width is derived from the smoothed High-Low range, scaled by a user-defined factor.
|
||||
The indicator applies a per-bar width adjustment based on the normalized range `w = (H-L)/(H+L)` before averaging. This means wider-range bars contribute proportionally more to band expansion. Three Simple Moving Averages (adjusted high, adjusted low, close) construct the bands.
|
||||
|
||||
### Calculation Steps
|
||||
### Calculation Steps (Headley's Formula)
|
||||
|
||||
1. **Component SMAs**:
|
||||
$$SMA_{High} = \frac{1}{n} \sum_{i=0}^{n-1} \text{High}_{t-i}$$
|
||||
$$SMA_{Low} = \frac{1}{n} \sum_{i=0}^{n-1} \text{Low}_{t-i}$$
|
||||
$$SMA_{Close} = \frac{1}{n} \sum_{i=0}^{n-1} \text{Close}_{t-i}$$
|
||||
1. **Per-bar normalized width**:
|
||||
$$w_t = \frac{High_t - Low_t}{High_t + Low_t}$$
|
||||
|
||||
2. **Band Width**:
|
||||
$$Width_t = (SMA_{High} - SMA_{Low}) \times Factor$$
|
||||
2. **Adjusted prices per bar**:
|
||||
$$AdjHigh_t = High_t \times (1 + Factor \times w_t)$$
|
||||
$$AdjLow_t = Low_t \times (1 - Factor \times w_t)$$
|
||||
|
||||
3. **Band Construction**:
|
||||
$$Upper_t = SMA_{High} + Width_t$$
|
||||
$$Lower_t = SMA_{Low} - Width_t$$
|
||||
$$Middle_t = SMA_{Close}$$
|
||||
$$Upper_t = SMA(AdjHigh, n)$$
|
||||
$$Lower_t = SMA(AdjLow, n)$$
|
||||
$$Middle_t = SMA(Close, n)$$
|
||||
|
||||
Where $n$ = period (default 20), $Factor$ = multiplier (default 2.0).
|
||||
Where $n$ = period (default 20), $Factor$ = multiplier (default 4.0).
|
||||
|
||||
## Performance Profile
|
||||
|
||||
The implementation uses three independent circular buffers (High, Low, Close) to maintain O(1) complexity for the moving averages.
|
||||
The implementation uses three independent circular buffers (adjusted high, adjusted low, close) to maintain O(1) complexity for the moving averages.
|
||||
|
||||
### Operation Count - Single value
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| ADD/SUB | 8 | 1 | 8 |
|
||||
| MUL | 2 | 3 | 6 |
|
||||
| DIV | 3 | 15 | 45 |
|
||||
| **Total** | **13** | — | **~59 cycles** |
|
||||
| ADD/SUB | 10 | 1 | 10 |
|
||||
| MUL | 4 | 3 | 12 |
|
||||
| DIV | 4 | 15 | 60 |
|
||||
| **Total** | **18** | — | **~82 cycles** |
|
||||
|
||||
### Operation Count - Batch processing
|
||||
|
||||
SIMD optimization is applied to the final band construction, though the recursive nature of the SMAs limits full vectorization of the state maintenance.
|
||||
SIMD optimization is applied to the sum resynchronization, though the recursive nature of the SMAs limits full vectorization of the state maintenance.
|
||||
|
||||
| Operation | Scalar Ops | SIMD Ops (AVX/SSE) | Acceleration |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
@@ -55,16 +54,16 @@ SIMD optimization is applied to the final band construction, though the recursiv
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **TA-Lib** | N/A | Not implemented |
|
||||
| **Skender** | ✅ | Matches `getAccelerationBands` |
|
||||
| **TA-Lib** | ✅ | All three bands match exactly (same Headley formula) |
|
||||
| **Internal** | ✅ | Streaming/Batch/Span match exactly |
|
||||
|
||||
## Usage & Pitfalls
|
||||
|
||||
- **Trend Definition**: Headley defines a breakout as two consecutive closes outside the bands.
|
||||
- **Parameter Sensitivity**: The default factor of 2.0 is tuned for equities. Crypto or FX may require higher factors (e.g., 3.0) due to "fat tails" in intra-bar range.
|
||||
- **Parameter Sensitivity**: The default factor of 4.0 matches TA-Lib and Headley's original. Lower factors (e.g., 2.0) produce tighter bands; higher factors (e.g., 6.0) may be needed for crypto/FX.
|
||||
- **Lag**: Inherits the lag of the underlying SMA. Not suitable for ultra-high-frequency reacting.
|
||||
- **Range vs Variance**: Because it uses High-Low range, it is more sensitive to "wicks" or momentary spikes than close-based envelopes.
|
||||
- **Division by Zero**: When High + Low = 0 (price is zero), the normalized width defaults to 0.
|
||||
|
||||
## API
|
||||
|
||||
@@ -87,14 +86,14 @@ classDiagram
|
||||
| Parameter | Type | Default | Range | Description |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| `period` | `int` | — | `>0` | Lookback period for SMAs. |
|
||||
| `factor` | `double` | `2.0` | `>0` | Multiplier for band width. |
|
||||
| `factor` | `double` | `4.0` | `>0` | Multiplier for normalized width. |
|
||||
| `source` | `TBarSeries` | — | `any` | Initial input TBar data (optional). |
|
||||
|
||||
### Properties
|
||||
|
||||
- `Last` (`TValue`): The current middle band value (SMA of Close).
|
||||
- `Upper` (`TValue`): The current upper band value.
|
||||
- `Lower` (`TValue`): The current lower band value.
|
||||
- `Upper` (`TValue`): The current upper band value (SMA of adjusted High).
|
||||
- `Lower` (`TValue`): The current lower band value (SMA of adjusted Low).
|
||||
- `IsHot` (`bool`): Returns `true` if valid data is available (warmup complete).
|
||||
|
||||
### Methods
|
||||
@@ -109,7 +108,7 @@ classDiagram
|
||||
using QuanTAlib;
|
||||
|
||||
// Initialize
|
||||
var indicator = new AccBands(period: 20, factor: 2.0);
|
||||
var indicator = new AccBands(period: 20, factor: 4.0);
|
||||
|
||||
// Update Loop
|
||||
foreach (var bar in bars)
|
||||
|
||||
@@ -3,56 +3,60 @@
|
||||
//@version=6
|
||||
indicator("Acceleration Bands (ACCBANDS)", "ACCBANDS", overlay=true)
|
||||
|
||||
//@function Calculates Acceleration Bands using SMAs of high, low, close prices
|
||||
//@function Calculates Acceleration Bands using Price Headley's original formula
|
||||
//@param high Series of high prices
|
||||
//@param low Series of low prices
|
||||
//@param close Series of close prices
|
||||
//@param period Lookback period for the moving average
|
||||
//@param factor Multiplier for band width calculation
|
||||
//@param factor Multiplier for normalized width (default 4.0 per Headley)
|
||||
//@returns tuple with [middle, upper, lower] band values
|
||||
//@optimized Uses circular buffers with O(1) complexity per bar
|
||||
accbands(series float high, series float low, series float close, simple int period, simple float factor = 2.0) =>
|
||||
accbands(series float high, series float low, series float close, simple int period, simple float factor = 4.0) =>
|
||||
if period <= 0 or factor <= 0.0
|
||||
runtime.error("Period and factor must be greater than 0")
|
||||
var int p = math.max(1, period)
|
||||
var int head = 0
|
||||
var int count = 0
|
||||
var array<float> bufferHigh = array.new_float(p, na)
|
||||
var array<float> bufferLow = array.new_float(p, na)
|
||||
var array<float> bufferAdjHigh = array.new_float(p, na)
|
||||
var array<float> bufferAdjLow = array.new_float(p, na)
|
||||
var array<float> bufferClose = array.new_float(p, na)
|
||||
var float sumHigh = 0.0
|
||||
var float sumLow = 0.0
|
||||
var float sumAdjHigh = 0.0
|
||||
var float sumAdjLow = 0.0
|
||||
var float sumClose = 0.0
|
||||
float oldestHigh = array.get(bufferHigh, head)
|
||||
float oldestLow = array.get(bufferLow, head)
|
||||
float oldestAdjHigh = array.get(bufferAdjHigh, head)
|
||||
float oldestAdjLow = array.get(bufferAdjLow, head)
|
||||
float oldestClose = array.get(bufferClose, head)
|
||||
if not na(oldestHigh)
|
||||
sumHigh -= oldestHigh
|
||||
sumLow -= oldestLow
|
||||
if not na(oldestAdjHigh)
|
||||
sumAdjHigh -= oldestAdjHigh
|
||||
sumAdjLow -= oldestAdjLow
|
||||
sumClose -= oldestClose
|
||||
count -= 1
|
||||
float currentHigh = nz(high)
|
||||
float currentLow = nz(low)
|
||||
float currentClose = nz(close)
|
||||
sumHigh += currentHigh
|
||||
sumLow += currentLow
|
||||
// Headley's per-bar normalized width
|
||||
float denom = currentHigh + currentLow
|
||||
float w = denom != 0.0 ? (currentHigh - currentLow) / denom : 0.0
|
||||
float adjHigh = currentHigh * (1.0 + factor * w)
|
||||
float adjLow = currentLow * (1.0 - factor * w)
|
||||
sumAdjHigh += adjHigh
|
||||
sumAdjLow += adjLow
|
||||
sumClose += currentClose
|
||||
count += 1
|
||||
array.set(bufferHigh, head, currentHigh)
|
||||
array.set(bufferLow, head, currentLow)
|
||||
array.set(bufferAdjHigh, head, adjHigh)
|
||||
array.set(bufferAdjLow, head, adjLow)
|
||||
array.set(bufferClose, head, currentClose)
|
||||
head := (head + 1) % p
|
||||
float smaHigh = nz(sumHigh / count)
|
||||
float smaLow = nz(sumLow / count)
|
||||
float smaAdjHigh = nz(sumAdjHigh / count)
|
||||
float smaAdjLow = nz(sumAdjLow / count)
|
||||
float smaClose = nz(sumClose / count)
|
||||
float bandWidth = (smaHigh - smaLow) * factor
|
||||
[smaClose, smaHigh + bandWidth, smaLow - bandWidth]
|
||||
[smaClose, smaAdjHigh, smaAdjLow]
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_period = input.int(20, "Period", minval=1)
|
||||
i_factor = input.float(2.0, "Factor", minval=0.001)
|
||||
i_factor = input.float(4.0, "Factor", minval=0.001)
|
||||
|
||||
// Calculation
|
||||
[middle, upper, lower] = accbands(high, low, close, i_period, i_factor)
|
||||
|
||||
@@ -1,56 +1,54 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Andrews' Pitchfork (AP)", "AP", overlay=true)
|
||||
indicator("Adaptive Price Channel (APCHANNEL)", "APCHANNEL", overlay=true)
|
||||
|
||||
//@function Calculates Andrews' Pitchfork lines based on three pivot points
|
||||
//@param p1_back Bars back to first pivot point (leftmost)
|
||||
//@param p2_back Bars back to second pivot point (middle)
|
||||
//@param p3_back Bars back to third pivot point (rightmost)
|
||||
//@returns tuple of [median, upper, lower] lines for current bar
|
||||
//@optimized Geometric projection with O(1) complexity per bar
|
||||
apchannel(simple int p1_back, simple int p2_back, simple int p3_back) =>
|
||||
if p1_back <= 0 or p2_back <= 0 or p3_back <= 0 or not (p1_back > p2_back and p2_back > p3_back)
|
||||
runtime.error("Use P1 oldest, P2 newer, P3 newest — all >0")
|
||||
[na, na, na]
|
||||
int p1_b = math.min(p1_back, bar_index)
|
||||
int p2_b = math.min(p2_back, bar_index)
|
||||
int p3_b = math.min(p3_back, bar_index)
|
||||
int p1_time = bar_index - p1_b
|
||||
int p2_time = bar_index - p2_b
|
||||
int p3_time = bar_index - p3_b
|
||||
float p1_price = nz(close[p1_b])
|
||||
float p2_price = nz(high[p2_b])
|
||||
float p3_price = nz(low[p3_b])
|
||||
if na(close[p1_b]) or na(high[p2_b]) or na(low[p3_b])
|
||||
//@function Calculates the Adaptive Price Channel using dual EMA on highs and lows.
|
||||
//@doc The channel applies exponential smoothing to price highs and lows independently,
|
||||
// creating a dynamic envelope that "remembers" significant extremes while gradually
|
||||
// fading their influence. Unlike fixed-window Donchian channels that drop extremes
|
||||
// abruptly, APCHANNEL decays them smoothly (leaky integration).
|
||||
//@param alpha Smoothing factor (0 < alpha <= 1). Higher = faster decay, shorter memory.
|
||||
//@returns tuple of [middle, upper, lower] band values
|
||||
//@optimized O(1) per bar via EMA recursion; uses FMA pattern: decay*prev + alpha*new
|
||||
apchannel(simple float alpha) =>
|
||||
if alpha <= 0.0 or alpha > 1.0
|
||||
runtime.error("Alpha must be > 0 and <= 1")
|
||||
[float(na), float(na), float(na)]
|
||||
float mid_time_float = (float(p2_time) + float(p3_time)) / 2.0
|
||||
float mid_price = (p2_price + p3_price) / 2.0
|
||||
float time_diff = mid_time_float - float(p1_time)
|
||||
float median_slope = math.abs(time_diff) > 1e-10 ? (mid_price - p1_price) / time_diff : 0.0
|
||||
float median_value = p1_price + median_slope * (float(bar_index) - float(p1_time))
|
||||
float upper_value = p2_price + median_slope * (float(bar_index) - float(p2_time))
|
||||
float lower_value = p3_price + median_slope * (float(bar_index) - float(p3_time))
|
||||
if math.abs(median_value) > 1e9 or math.abs(upper_value) > 1e9 or math.abs(lower_value) > 1e9
|
||||
[float(na), float(na), float(na)]
|
||||
[median_value, upper_value, lower_value]
|
||||
|
||||
float decay = 1.0 - alpha
|
||||
|
||||
// EMA of highs (upper band)
|
||||
var float high_ema = na
|
||||
float valid_high = nz(high, nz(high_ema, 0.0))
|
||||
if na(high_ema)
|
||||
high_ema := valid_high
|
||||
else
|
||||
high_ema := decay * high_ema + alpha * valid_high
|
||||
|
||||
// EMA of lows (lower band)
|
||||
var float low_ema = na
|
||||
float valid_low = nz(low, nz(low_ema, 0.0))
|
||||
if na(low_ema)
|
||||
low_ema := valid_low
|
||||
else
|
||||
low_ema := decay * low_ema + alpha * valid_low
|
||||
|
||||
// Midpoint
|
||||
float mid = (high_ema + low_ema) / 2.0
|
||||
|
||||
[mid, high_ema, low_ema]
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_p1_back = input.int(45, "Point 1 (Leftmost)", minval=1)
|
||||
i_p2_back = input.int(30, "Point 2 (Second)", minval=1)
|
||||
i_p3_back = input.int(15, "Point 3 (Third)", minval=1)
|
||||
|
||||
// Validation
|
||||
if i_p1_back <= i_p2_back or i_p2_back <= i_p3_back
|
||||
runtime.error("Points must be in chronological order (P1 > P2 > P3)")
|
||||
i_alpha = input.float(0.2, "Alpha (smoothing factor)", minval=0.01, maxval=1.0, step=0.01)
|
||||
|
||||
// Calculation
|
||||
[median, upper, lower] = apchannel(i_p1_back, i_p2_back, i_p3_back)
|
||||
[middle, upper, lower] = apchannel(i_alpha)
|
||||
|
||||
// Plot
|
||||
plot(median, "Median", color=color.yellow, linewidth=2)
|
||||
plot(middle, "Middle", color=color.yellow, linewidth=2)
|
||||
p1 = plot(upper, "Upper", color=color.new(color.blue, 50), linewidth=1)
|
||||
p2 = plot(lower, "Lower", color=color.new(color.blue, 50), linewidth=1)
|
||||
fill(p1, p2, color=color.new(color.blue, 90))
|
||||
|
||||
@@ -295,9 +295,17 @@ public sealed class Decaychannel : ITValuePublisher
|
||||
double top = Math.Min(decayedMax, rawMax);
|
||||
double bot = Math.Max(decayedMin, rawMin);
|
||||
|
||||
// Guard: aggressive decay can cause bot > top; clamp to midpoint
|
||||
if (bot > top)
|
||||
{
|
||||
double clamp = (top + bot) * 0.5;
|
||||
top = clamp;
|
||||
bot = clamp;
|
||||
}
|
||||
|
||||
// Update tracked values for next iteration
|
||||
_currentMax = Math.Max(top, rawMax);
|
||||
_currentMin = Math.Min(bot, rawMin);
|
||||
_currentMax = top;
|
||||
_currentMin = bot;
|
||||
|
||||
double mid = (top + bot) * 0.5;
|
||||
|
||||
@@ -477,8 +485,16 @@ public sealed class Decaychannel : ITValuePublisher
|
||||
double top = Math.Min(decayedMax, rawMax);
|
||||
double bot = Math.Max(decayedMin, rawMin);
|
||||
|
||||
currentMax = Math.Max(top, rawMax);
|
||||
currentMin = Math.Min(bot, rawMin);
|
||||
// Guard: aggressive decay can cause bot > top; clamp to midpoint
|
||||
if (bot > top)
|
||||
{
|
||||
double clamp = (top + bot) * 0.5;
|
||||
top = clamp;
|
||||
bot = clamp;
|
||||
}
|
||||
|
||||
currentMax = top;
|
||||
currentMin = bot;
|
||||
|
||||
double mid = (top + bot) * 0.5;
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ public class JbandsIndicatorTests
|
||||
|
||||
Assert.Equal(7, ind.Period);
|
||||
Assert.Equal(0, ind.Phase);
|
||||
Assert.Equal(0.45, ind.Power);
|
||||
Assert.True(ind.ShowColdValues);
|
||||
Assert.Equal("Jbands - Jurik Adaptive Envelope Bands", ind.Name);
|
||||
Assert.False(ind.SeparateWindow);
|
||||
@@ -171,32 +170,30 @@ public class JbandsIndicatorTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Power_Parameter_Stored_Correctly()
|
||||
public void Phase_Parameter_Stored_Correctly()
|
||||
{
|
||||
// Power parameter is accepted and stored but not currently used in Jbands calculation.
|
||||
// This test verifies the parameter is properly stored and accessible.
|
||||
var indLow = new JbandsIndicator { Period = 7, Power = 0.3 };
|
||||
var indHigh = new JbandsIndicator { Period = 7, Power = 0.8 };
|
||||
var indPos = new JbandsIndicator { Period = 7, Phase = 50 };
|
||||
var indNeg = new JbandsIndicator { Period = 7, Phase = -50 };
|
||||
|
||||
Assert.Equal(0.3, indLow.Power);
|
||||
Assert.Equal(0.8, indHigh.Power);
|
||||
Assert.Equal(50, indPos.Phase);
|
||||
Assert.Equal(-50, indNeg.Phase);
|
||||
|
||||
// Verify both indicators produce valid output
|
||||
indLow.Initialize();
|
||||
indHigh.Initialize();
|
||||
indPos.Initialize();
|
||||
indNeg.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double price = 100 + Math.Sin(i * 0.3) * 10;
|
||||
indLow.HistoricalData.AddBar(now.AddMinutes(i), price - 1, price + 2, price - 2, price);
|
||||
indHigh.HistoricalData.AddBar(now.AddMinutes(i), price - 1, price + 2, price - 2, price);
|
||||
indLow.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
indHigh.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
indPos.HistoricalData.AddBar(now.AddMinutes(i), price - 1, price + 2, price - 2, price);
|
||||
indNeg.HistoricalData.AddBar(now.AddMinutes(i), price - 1, price + 2, price - 2, price);
|
||||
indPos.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
indNeg.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
// Both should produce finite values
|
||||
Assert.True(double.IsFinite(indLow.LinesSeries[0].GetValue(0)));
|
||||
Assert.True(double.IsFinite(indHigh.LinesSeries[0].GetValue(0)));
|
||||
Assert.True(double.IsFinite(indPos.LinesSeries[0].GetValue(0)));
|
||||
Assert.True(double.IsFinite(indNeg.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,6 @@ public sealed class JbandsIndicator : Indicator, IWatchlistIndicator
|
||||
[InputParameter("Phase", sortIndex: 20, minimum: -100, maximum: 100, increment: 1, decimalPlaces: 0)]
|
||||
public int Phase { get; set; } = 0;
|
||||
|
||||
[InputParameter("Power", sortIndex: 30, minimum: 0.01, maximum: 5.0, increment: 0.01, decimalPlaces: 2)]
|
||||
public double Power { get; set; } = 0.45;
|
||||
|
||||
[InputParameter("Show Cold Values", sortIndex: 100)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
@@ -39,7 +36,7 @@ public sealed class JbandsIndicator : Indicator, IWatchlistIndicator
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_indicator = new Jbands(Period, Phase, Power);
|
||||
_indicator = new Jbands(Period, Phase);
|
||||
|
||||
AddLineSeries(new LineSeries("Middle", Color.DodgerBlue, 2, LineStyle.Solid));
|
||||
AddLineSeries(new LineSeries("Upper", Color.FromArgb(255, 180, 180), 1, LineStyle.Dash));
|
||||
|
||||
@@ -11,7 +11,7 @@ public class JbandsTests
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Jbands(0));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Jbands(-5));
|
||||
Assert.Throws<ArgumentException>(() => new Jbands(14, 0, double.NaN));
|
||||
// power parameter removed - no longer applicable
|
||||
|
||||
var j = new Jbands(14);
|
||||
Assert.Contains("Jbands", j.Name, StringComparison.OrdinalIgnoreCase);
|
||||
@@ -19,10 +19,10 @@ public class JbandsTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jbands_Constructor_InfinityPower_Throws()
|
||||
public void Jbands_Constructor_Period1_IsValid()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Jbands(14, 0, double.PositiveInfinity));
|
||||
Assert.Throws<ArgumentException>(() => new Jbands(14, 0, double.NegativeInfinity));
|
||||
var j = new Jbands(1);
|
||||
Assert.True(j.WarmupPeriod > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -276,7 +276,7 @@ public class JbandsTests
|
||||
[Fact]
|
||||
public void Jbands_Reset_ThenReuse_ProducesSameResults()
|
||||
{
|
||||
var j = new Jbands(14, 0, 0.45);
|
||||
var j = new Jbands(14, 0);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 88);
|
||||
double[] prices = new double[100];
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
@@ -424,8 +424,8 @@ public class JbandsTests
|
||||
[Fact]
|
||||
public void Jbands_Prime_MatchesStreamingResults()
|
||||
{
|
||||
var jPrime = new Jbands(14, 0, 0.45);
|
||||
var jStream = new Jbands(14, 0, 0.45);
|
||||
var jPrime = new Jbands(14, 0);
|
||||
var jStream = new Jbands(14, 0);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
|
||||
|
||||
var series = new TSeries();
|
||||
@@ -459,7 +459,7 @@ public class JbandsTests
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
var (results, indicator) = Jbands.Calculate(series, 14, 0, 0.45);
|
||||
var (results, indicator) = Jbands.Calculate(series, 14, 0);
|
||||
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.Equal(300, results.Middle.Count);
|
||||
@@ -513,7 +513,7 @@ public class JbandsTests
|
||||
[Fact]
|
||||
public void Jbands_BatchVsStreaming_Match()
|
||||
{
|
||||
var jStream = new Jbands(14, 0, 0.45);
|
||||
var jStream = new Jbands(14, 0);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var series = new TSeries();
|
||||
|
||||
@@ -528,7 +528,7 @@ public class JbandsTests
|
||||
double expectedUp = jStream.Upper.Value;
|
||||
double expectedLo = jStream.Lower.Value;
|
||||
|
||||
var (midBatch, upBatch, loBatch) = Jbands.Batch(series, 14, 0, 0.45);
|
||||
var (midBatch, upBatch, loBatch) = Jbands.Batch(series, 14, 0);
|
||||
|
||||
Assert.Equal(expectedMid, midBatch.Last.Value, 1e-10);
|
||||
Assert.Equal(expectedUp, upBatch.Last.Value, 1e-10);
|
||||
@@ -634,7 +634,7 @@ public class JbandsTests
|
||||
public void Jbands_MiddleBand_MatchesJma()
|
||||
{
|
||||
// Verify that middle band matches standalone JMA
|
||||
var jbands = new Jbands(14, 0, 0.45);
|
||||
var jbands = new Jbands(14, 0);
|
||||
var jma = new Jma(14, 0, 0.45);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 999);
|
||||
|
||||
|
||||
@@ -16,34 +16,34 @@ public class JbandsValidationTests
|
||||
[Fact]
|
||||
public void Jbands_MiddleBand_MatchesJma_Period7()
|
||||
{
|
||||
ValidateMiddleBandMatchesJma(7, 0, 0.45, 42);
|
||||
ValidateMiddleBandMatchesJma(7, 0, 42);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jbands_MiddleBand_MatchesJma_Period14()
|
||||
{
|
||||
ValidateMiddleBandMatchesJma(14, 0, 0.45, 123);
|
||||
ValidateMiddleBandMatchesJma(14, 0, 123);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jbands_MiddleBand_MatchesJma_Period20()
|
||||
{
|
||||
ValidateMiddleBandMatchesJma(20, 0, 0.45, 456);
|
||||
ValidateMiddleBandMatchesJma(20, 0, 456);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jbands_MiddleBand_MatchesJma_WithPhase()
|
||||
{
|
||||
ValidateMiddleBandMatchesJma(14, 50, 0.45, 789);
|
||||
ValidateMiddleBandMatchesJma(14, -50, 0.45, 321);
|
||||
ValidateMiddleBandMatchesJma(14, 100, 0.45, 654);
|
||||
ValidateMiddleBandMatchesJma(14, -100, 0.45, 987);
|
||||
ValidateMiddleBandMatchesJma(14, 50, 789);
|
||||
ValidateMiddleBandMatchesJma(14, -50, 321);
|
||||
ValidateMiddleBandMatchesJma(14, 100, 654);
|
||||
ValidateMiddleBandMatchesJma(14, -100, 987);
|
||||
}
|
||||
|
||||
private static void ValidateMiddleBandMatchesJma(int period, int phase, double power, int seed)
|
||||
private static void ValidateMiddleBandMatchesJma(int period, int phase, int seed)
|
||||
{
|
||||
var jbands = new Jbands(period, phase, power);
|
||||
var jma = new Jma(period, phase, power);
|
||||
var jbands = new Jbands(period, phase);
|
||||
var jma = new Jma(period, phase);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: seed);
|
||||
|
||||
for (int i = 0; i < 500; i++)
|
||||
@@ -60,7 +60,7 @@ public class JbandsValidationTests
|
||||
[Fact]
|
||||
public void Jbands_StreamingVsBatch_Match()
|
||||
{
|
||||
var jStream = new Jbands(14, 0, 0.45);
|
||||
var jStream = new Jbands(14, 0);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var series = new TSeries();
|
||||
|
||||
@@ -71,13 +71,13 @@ public class JbandsValidationTests
|
||||
jStream.Update(new TValue(bar.Time, bar.Close), isNew: true);
|
||||
}
|
||||
|
||||
var (midBatch, upBatch, loBatch) = Jbands.Batch(series, 14, 0, 0.45);
|
||||
var (midBatch, upBatch, loBatch) = Jbands.Batch(series, 14, 0);
|
||||
|
||||
// Compare last 100 values
|
||||
for (int i = series.Count - 100; i < series.Count; i++)
|
||||
{
|
||||
// Rebuild streaming to get value at index i
|
||||
var jCheck = new Jbands(14, 0, 0.45);
|
||||
var jCheck = new Jbands(14, 0);
|
||||
for (int j = 0; j <= i; j++)
|
||||
{
|
||||
jCheck.Update(new TValue(new DateTime(series.Times[j], DateTimeKind.Utc), series.Values[j]), isNew: true);
|
||||
@@ -131,23 +131,23 @@ public class JbandsValidationTests
|
||||
}
|
||||
|
||||
// Mode 1: Streaming
|
||||
var jStream = new Jbands(14, 25, 0.45);
|
||||
var jStream = new Jbands(14, 25);
|
||||
for (int i = 0; i < rawValues.Length; i++)
|
||||
{
|
||||
jStream.Update(new TValue(DateTime.UtcNow, rawValues[i]), isNew: true);
|
||||
}
|
||||
|
||||
// Mode 2: Batch (TSeries)
|
||||
var (midBatch, upBatch, loBatch) = Jbands.Batch(series, 14, 25, 0.45);
|
||||
var (midBatch, upBatch, loBatch) = Jbands.Batch(series, 14, 25);
|
||||
|
||||
// Mode 3: Span Calculate
|
||||
double[] middleSpan = new double[150];
|
||||
double[] upperSpan = new double[150];
|
||||
double[] lowerSpan = new double[150];
|
||||
Jbands.Batch(rawValues.AsSpan(), middleSpan.AsSpan(), upperSpan.AsSpan(), lowerSpan.AsSpan(), 14, 25, 0.45);
|
||||
Jbands.Batch(rawValues.AsSpan(), middleSpan.AsSpan(), upperSpan.AsSpan(), lowerSpan.AsSpan(), 14, 25);
|
||||
|
||||
// Mode 4: Event-based
|
||||
var jEvent = new Jbands(14, 25, 0.45);
|
||||
var jEvent = new Jbands(14, 25);
|
||||
double lastEventMid = 0, lastEventUp = 0, lastEventLo = 0;
|
||||
jEvent.Pub += (object? sender, in TValueEventArgs args) =>
|
||||
{
|
||||
|
||||
@@ -60,18 +60,13 @@ public sealed class Jbands : ITValuePublisher, IDisposable
|
||||
|
||||
public event TValuePublishedHandler? Pub;
|
||||
|
||||
public Jbands(int period, int phase = 0, double power = 0.45)
|
||||
public Jbands(int period, int phase = 0)
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1.");
|
||||
}
|
||||
|
||||
if (!double.IsFinite(power))
|
||||
{
|
||||
throw new ArgumentException("Power must be finite.", nameof(power));
|
||||
}
|
||||
|
||||
// Phase parameter: maps -100..100 -> 0.5..2.5
|
||||
if (phase < -100)
|
||||
{
|
||||
@@ -107,7 +102,7 @@ public sealed class Jbands : ITValuePublisher, IDisposable
|
||||
WarmupPeriod = (int)Math.Ceiling(20.0 + 80.0 * Math.Pow(period, 0.36));
|
||||
|
||||
_handler = Handle;
|
||||
Name = $"Jbands({period},{phase},{power})";
|
||||
Name = $"Jbands({period},{phase})";
|
||||
|
||||
_devBuffer = new RingBuffer(DevWindowSize);
|
||||
_volBuffer = new RingBuffer(VolWindowSize);
|
||||
@@ -115,8 +110,8 @@ public sealed class Jbands : ITValuePublisher, IDisposable
|
||||
Reset();
|
||||
}
|
||||
|
||||
public Jbands(ITValuePublisher source, int period, int phase = 0, double power = 0.45)
|
||||
: this(period, phase, power)
|
||||
public Jbands(ITValuePublisher source, int period, int phase = 0)
|
||||
: this(period, phase)
|
||||
{
|
||||
_source = source;
|
||||
source.Pub += _handler;
|
||||
@@ -380,9 +375,9 @@ public sealed class Jbands : ITValuePublisher, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public static (TSeries Middle, TSeries Upper, TSeries Lower) Batch(TSeries source, int period, int phase = 0, double power = 0.45)
|
||||
public static (TSeries Middle, TSeries Upper, TSeries Lower) Batch(TSeries source, int period, int phase = 0)
|
||||
{
|
||||
var jbands = new Jbands(period, phase, power);
|
||||
var jbands = new Jbands(period, phase);
|
||||
return jbands.Update(source);
|
||||
}
|
||||
|
||||
@@ -392,8 +387,7 @@ public sealed class Jbands : ITValuePublisher, IDisposable
|
||||
Span<double> upper,
|
||||
Span<double> lower,
|
||||
int period,
|
||||
int phase = 0,
|
||||
double power = 0.45)
|
||||
int phase = 0)
|
||||
{
|
||||
if (middle.Length != source.Length)
|
||||
{
|
||||
@@ -415,7 +409,7 @@ public sealed class Jbands : ITValuePublisher, IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
var jbands = new Jbands(period, phase, power);
|
||||
var jbands = new Jbands(period, phase);
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
var (jma, u, l) = jbands.Step(source[i], isNew: true);
|
||||
@@ -428,9 +422,9 @@ public sealed class Jbands : ITValuePublisher, IDisposable
|
||||
/// <summary>
|
||||
/// Calculates Jbands and returns both the results and the indicator instance.
|
||||
/// </summary>
|
||||
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Jbands Indicator) Calculate(TSeries source, int period, int phase = 0, double power = 0.45)
|
||||
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Jbands Indicator) Calculate(TSeries source, int period, int phase = 0)
|
||||
{
|
||||
var indicator = new Jbands(period, phase, power);
|
||||
var indicator = new Jbands(period, phase);
|
||||
var results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
|
||||
@@ -14,9 +14,12 @@ namespace QuanTAlib;
|
||||
/// </summary>
|
||||
public sealed class StarchannelIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 10, minimum: 1, maximum: 500, increment: 1, decimalPlaces: 0)]
|
||||
[InputParameter("SMA Period", sortIndex: 10, minimum: 1, maximum: 500, increment: 1, decimalPlaces: 0)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[InputParameter("ATR Period (0 = same as SMA)", sortIndex: 15, minimum: 0, maximum: 500, increment: 1, decimalPlaces: 0)]
|
||||
public int AtrPeriod { get; set; } = 0;
|
||||
|
||||
[InputParameter("Multiplier", sortIndex: 20, minimum: 0.1, maximum: 10.0, increment: 0.1, decimalPlaces: 1)]
|
||||
public double Multiplier { get; set; } = 2.0;
|
||||
|
||||
@@ -25,8 +28,10 @@ public sealed class StarchannelIndicator : Indicator, IWatchlistIndicator
|
||||
|
||||
private Starchannel? _indicator;
|
||||
|
||||
public int MinHistoryDepths => Period;
|
||||
public override string ShortName => $"Starchannel({Period},{Multiplier})";
|
||||
public int MinHistoryDepths => Math.Max(Period, AtrPeriod > 0 ? AtrPeriod : Period);
|
||||
public override string ShortName => AtrPeriod > 0 && AtrPeriod != Period
|
||||
? $"Starchannel({Period},{Multiplier},{AtrPeriod})"
|
||||
: $"Starchannel({Period},{Multiplier})";
|
||||
|
||||
public StarchannelIndicator()
|
||||
{
|
||||
@@ -38,7 +43,7 @@ public sealed class StarchannelIndicator : Indicator, IWatchlistIndicator
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_indicator = new Starchannel(Period, Multiplier);
|
||||
_indicator = new Starchannel(Period, Multiplier, AtrPeriod);
|
||||
|
||||
AddLineSeries(new LineSeries("Middle", Color.DodgerBlue, 2, LineStyle.Solid));
|
||||
AddLineSeries(new LineSeries("Upper", Color.FromArgb(255, 180, 180), 1, LineStyle.Dash));
|
||||
|
||||
@@ -7,14 +7,16 @@ namespace QuanTAlib;
|
||||
/// STARCHANNEL: Stoller Average Range Channel
|
||||
/// A volatility-based envelope using SMA as the middle line and ATR for band width.
|
||||
/// Middle = SMA(source, period)
|
||||
/// Upper = Middle + (multiplier × ATR)
|
||||
/// Lower = Middle - (multiplier × ATR)
|
||||
/// Upper = Middle + (multiplier × ATR(atrPeriod))
|
||||
/// Lower = Middle - (multiplier × ATR(atrPeriod))
|
||||
/// ATR uses RMA (Wilder's smoothing) with warmup compensation.
|
||||
/// Supports separate SMA and ATR periods for traditional Stoller dual-period design.
|
||||
/// </summary>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Starchannel : ITValuePublisher
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly int _atrPeriod;
|
||||
private readonly double _multiplier;
|
||||
private readonly double _atrAlpha;
|
||||
private readonly RingBuffer _smaBuffer;
|
||||
@@ -46,7 +48,7 @@ public sealed class Starchannel : ITValuePublisher
|
||||
|
||||
public event TValuePublishedHandler? Pub;
|
||||
|
||||
public Starchannel(int period = 20, double multiplier = 2.0)
|
||||
public Starchannel(int period = 20, double multiplier = 2.0, int atrPeriod = 0)
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
@@ -58,20 +60,30 @@ public sealed class Starchannel : ITValuePublisher
|
||||
throw new ArgumentOutOfRangeException(nameof(multiplier), "Multiplier must be > 0.");
|
||||
}
|
||||
|
||||
// Default atrPeriod to period when 0 (backward compatible)
|
||||
int effectiveAtrPeriod = atrPeriod > 0 ? atrPeriod : period;
|
||||
if (effectiveAtrPeriod < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(atrPeriod), "ATR period must be >= 1.");
|
||||
}
|
||||
|
||||
_period = period;
|
||||
_atrPeriod = effectiveAtrPeriod;
|
||||
_multiplier = multiplier;
|
||||
_atrAlpha = 1.0 / period;
|
||||
_atrAlpha = 1.0 / effectiveAtrPeriod;
|
||||
_smaBuffer = new RingBuffer(period);
|
||||
|
||||
WarmupPeriod = period;
|
||||
WarmupPeriod = Math.Max(period, effectiveAtrPeriod);
|
||||
|
||||
Name = $"Starchannel({period},{multiplier})";
|
||||
Name = effectiveAtrPeriod == period
|
||||
? $"Starchannel({period},{multiplier})"
|
||||
: $"Starchannel({period},{multiplier},{effectiveAtrPeriod})";
|
||||
_barHandler = HandleBar;
|
||||
|
||||
Reset();
|
||||
}
|
||||
|
||||
public Starchannel(TBarSeries source, int period = 20, double multiplier = 2.0) : this(period, multiplier)
|
||||
public Starchannel(TBarSeries source, int period = 20, double multiplier = 2.0, int atrPeriod = 0) : this(period, multiplier, atrPeriod)
|
||||
{
|
||||
Prime(source);
|
||||
source.Pub += _barHandler;
|
||||
@@ -179,8 +191,8 @@ public sealed class Starchannel : ITValuePublisher
|
||||
double tr3 = Math.Abs(low - prevClose);
|
||||
double trueRange = Math.Max(tr1, Math.Max(tr2, tr3));
|
||||
|
||||
// ATR using RMA with warmup compensation
|
||||
double newRawRma = (_state.RawRma * (_period - 1) + trueRange) / _period;
|
||||
// ATR using RMA with warmup compensation (uses _atrPeriod for separate ATR smoothing)
|
||||
double newRawRma = (_state.RawRma * (_atrPeriod - 1) + trueRange) / _atrPeriod;
|
||||
double newE = (1.0 - _atrAlpha) * _state.E;
|
||||
double atrValue = newE > Epsilon ? newRawRma / (1.0 - newE) : newRawRma;
|
||||
|
||||
@@ -238,7 +250,7 @@ public sealed class Starchannel : ITValuePublisher
|
||||
var vLowerSpan = CollectionsMarshal.AsSpan(vLower);
|
||||
|
||||
Batch(source.HighValues, source.LowValues, source.CloseValues,
|
||||
vMiddleSpan, vUpperSpan, vLowerSpan, _period, _multiplier);
|
||||
vMiddleSpan, vUpperSpan, vLowerSpan, _period, _multiplier, _atrPeriod);
|
||||
|
||||
source.Times.CopyTo(tSpan);
|
||||
tSpan.CopyTo(CollectionsMarshal.AsSpan(tUpper));
|
||||
@@ -281,7 +293,8 @@ public sealed class Starchannel : ITValuePublisher
|
||||
Span<double> upper,
|
||||
Span<double> lower,
|
||||
int period,
|
||||
double multiplier = 2.0)
|
||||
double multiplier = 2.0,
|
||||
int atrPeriod = 0)
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
@@ -293,6 +306,9 @@ public sealed class Starchannel : ITValuePublisher
|
||||
throw new ArgumentOutOfRangeException(nameof(multiplier), "Multiplier must be > 0.");
|
||||
}
|
||||
|
||||
// Default atrPeriod to period when 0 (backward compatible)
|
||||
int effectiveAtrPeriod = atrPeriod > 0 ? atrPeriod : period;
|
||||
|
||||
if (high.Length != low.Length || high.Length != close.Length)
|
||||
{
|
||||
throw new ArgumentException("High, Low, and Close spans must have the same length", nameof(high));
|
||||
@@ -309,7 +325,7 @@ public sealed class Starchannel : ITValuePublisher
|
||||
return;
|
||||
}
|
||||
|
||||
double atrAlpha = 1.0 / period;
|
||||
double atrAlpha = 1.0 / effectiveAtrPeriod;
|
||||
|
||||
// First bar - sanitize first values
|
||||
double lastValidClose = double.IsFinite(close[0]) ? close[0] : 0;
|
||||
@@ -389,8 +405,8 @@ public sealed class Starchannel : ITValuePublisher
|
||||
double tr3 = Math.Abs(l - prevClose);
|
||||
double tr = Math.Max(tr1, Math.Max(tr2, tr3));
|
||||
|
||||
// ATR (RMA with warmup compensation)
|
||||
rawRma = (rawRma * (period - 1) + tr) / period;
|
||||
// ATR (RMA with warmup compensation, uses effectiveAtrPeriod)
|
||||
rawRma = (rawRma * (effectiveAtrPeriod - 1) + tr) / effectiveAtrPeriod;
|
||||
e = (1.0 - atrAlpha) * e;
|
||||
double atr = e > Epsilon ? rawRma / (1.0 - e) : rawRma;
|
||||
|
||||
@@ -403,7 +419,7 @@ public sealed class Starchannel : ITValuePublisher
|
||||
}
|
||||
}
|
||||
|
||||
public static (TSeries Middle, TSeries Upper, TSeries Lower) Batch(TBarSeries source, int period = 20, double multiplier = 2.0)
|
||||
public static (TSeries Middle, TSeries Upper, TSeries Lower) Batch(TBarSeries source, int period = 20, double multiplier = 2.0, int atrPeriod = 0)
|
||||
{
|
||||
int len = source.Count;
|
||||
var tMiddle = new List<long>(len);
|
||||
@@ -424,7 +440,7 @@ public sealed class Starchannel : ITValuePublisher
|
||||
CollectionsMarshal.AsSpan(vMiddle),
|
||||
CollectionsMarshal.AsSpan(vUpper),
|
||||
CollectionsMarshal.AsSpan(vLower),
|
||||
period, multiplier);
|
||||
period, multiplier, atrPeriod);
|
||||
|
||||
source.Times.CopyTo(CollectionsMarshal.AsSpan(tMiddle));
|
||||
CollectionsMarshal.AsSpan(tMiddle).CopyTo(CollectionsMarshal.AsSpan(tUpper));
|
||||
@@ -433,9 +449,9 @@ public sealed class Starchannel : ITValuePublisher
|
||||
return (new TSeries(tMiddle, vMiddle), new TSeries(tUpper, vUpper), new TSeries(tLower, vLower));
|
||||
}
|
||||
|
||||
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Starchannel Indicator) Calculate(TBarSeries source, int period = 20, double multiplier = 2.0)
|
||||
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Starchannel Indicator) Calculate(TBarSeries source, int period = 20, double multiplier = 2.0, int atrPeriod = 0)
|
||||
{
|
||||
var indicator = new Starchannel(source, period, multiplier);
|
||||
var indicator = new Starchannel(source, period, multiplier, atrPeriod);
|
||||
var results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
|
||||
@@ -5,13 +5,15 @@ indicator("Stoller Average Range Channel (STARCHANNEL)", "STARCHANNEL", overlay=
|
||||
|
||||
//@function Calculates Stoller Average Range Channel using ATR for width and SMA for center
|
||||
//@param source Source series for the center line
|
||||
//@param length Period for ATR and SMA calculations
|
||||
//@param length Period for SMA calculation
|
||||
//@param multiplier ATR multiplier for band width
|
||||
//@param atr_length Period for ATR calculation (0 = same as length)
|
||||
//@returns tuple with [middle, upper, lower] band values
|
||||
//@optimized Uses circular buffer for SMA and ATR with compensator, O(1) complexity
|
||||
starchannel(series float source, simple int length, simple float multiplier) =>
|
||||
starchannel(series float source, simple int length, simple float multiplier, simple int atr_length = 0) =>
|
||||
if length <= 0 or multiplier <= 0.0
|
||||
runtime.error("Length and multiplier must be greater than 0")
|
||||
int effective_atr_length = atr_length > 0 ? atr_length : length
|
||||
var float prevClose = close
|
||||
float tr1 = high - low
|
||||
float tr2 = math.abs(high - prevClose)
|
||||
@@ -44,8 +46,8 @@ starchannel(series float source, simple int length, simple float multiplier) =>
|
||||
var float e = 1.0
|
||||
float atrValue = na
|
||||
if not na(trueRange)
|
||||
float alpha = 1.0 / float(length)
|
||||
raw_rma := (raw_rma * (length - 1) + trueRange) / length
|
||||
float alpha = 1.0 / float(effective_atr_length)
|
||||
raw_rma := (raw_rma * (effective_atr_length - 1) + trueRange) / effective_atr_length
|
||||
e := (1.0 - alpha) * e
|
||||
atrValue := e > EPSILON ? raw_rma / (1.0 - e) : raw_rma
|
||||
float middleBand = nz(sumSource / count, source)
|
||||
@@ -56,11 +58,12 @@ starchannel(series float source, simple int length, simple float multiplier) =>
|
||||
|
||||
// Inputs
|
||||
i_source = input.source(close, "Source")
|
||||
i_length = input.int(20, "Length", minval=1)
|
||||
i_length = input.int(20, "SMA Length", minval=1)
|
||||
i_atr_length = input.int(0, "ATR Length (0 = same as SMA)", minval=0)
|
||||
i_mult = input.float(2.0, "ATR Multiplier", minval=0.001)
|
||||
|
||||
// Calculation
|
||||
[middle, upper, lower] = starchannel(i_source, i_length, i_mult)
|
||||
[middle, upper, lower] = starchannel(i_source, i_length, i_mult, i_atr_length)
|
||||
|
||||
// Plot
|
||||
plot(middle, "Middle", color=color.yellow, linewidth=2)
|
||||
|
||||
@@ -180,12 +180,11 @@ public class TtmLrcIndicatorTests
|
||||
var ind = new TtmLrcIndicator { Period = 10 };
|
||||
ind.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var rng = new Random(42);
|
||||
var bars = new GBM(seed: 42).Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double price = 100 + rng.NextDouble() * 20;
|
||||
ind.HistoricalData.AddBar(now.AddMinutes(i), price, price + 5, price - 5, price);
|
||||
var bar = bars[i];
|
||||
ind.HistoricalData.AddBar(bar.AsDateTime, bar.Open, bar.High, bar.Low, bar.Close);
|
||||
ind.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
@@ -285,11 +284,11 @@ public class TtmLrcIndicatorTests
|
||||
ind.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var rng = new Random(42);
|
||||
var bars = new GBM(seed: 42).Fetch(20, now.Ticks, TimeSpan.FromMinutes(1));
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double price = 100 + rng.NextDouble() * 30;
|
||||
ind.HistoricalData.AddBar(now.AddMinutes(i), price, price + 5, price - 5, price);
|
||||
var bar = bars[i];
|
||||
ind.HistoricalData.AddBar(bar.AsDateTime, bar.Open, bar.High, bar.Low, bar.Close);
|
||||
ind.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
|
||||
@@ -96,12 +96,11 @@ public class TtmLrcTests
|
||||
public void Bands_Symmetry_Upper1AndLower1EquidistantFromMiddle()
|
||||
{
|
||||
var indicator = new TtmLrc(10);
|
||||
var now = DateTime.UtcNow;
|
||||
var rng = new Random(42);
|
||||
var bars = new GBM(seed: 42).Fetch(15, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
indicator.Update(new TValue(now.AddMinutes(i), 100 + rng.NextDouble() * 10), isNew: true);
|
||||
indicator.Update(bars.Close[i], isNew: true);
|
||||
}
|
||||
|
||||
double mid = indicator.Midline.Value;
|
||||
@@ -119,12 +118,11 @@ public class TtmLrcTests
|
||||
public void Bands_Symmetry_Upper2AndLower2EquidistantFromMiddle()
|
||||
{
|
||||
var indicator = new TtmLrc(10);
|
||||
var now = DateTime.UtcNow;
|
||||
var rng = new Random(42);
|
||||
var bars = new GBM(seed: 42).Fetch(15, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
indicator.Update(new TValue(now.AddMinutes(i), 100 + rng.NextDouble() * 10), isNew: true);
|
||||
indicator.Update(bars.Close[i], isNew: true);
|
||||
}
|
||||
|
||||
double mid = indicator.Midline.Value;
|
||||
@@ -142,12 +140,11 @@ public class TtmLrcTests
|
||||
public void Bands_Ordering_UpperGreaterThanMiddleGreaterThanLower()
|
||||
{
|
||||
var indicator = new TtmLrc(10);
|
||||
var now = DateTime.UtcNow;
|
||||
var rng = new Random(42);
|
||||
var bars = new GBM(seed: 42).Fetch(15, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
indicator.Update(new TValue(now.AddMinutes(i), 100 + rng.NextDouble() * 10), isNew: true);
|
||||
indicator.Update(bars.Close[i], isNew: true);
|
||||
}
|
||||
|
||||
Assert.True(indicator.Upper2.Value >= indicator.Upper1.Value, "Upper2 should be >= Upper1");
|
||||
@@ -249,12 +246,11 @@ public class TtmLrcTests
|
||||
public void RSquared_RandomData_LessThanOne()
|
||||
{
|
||||
var indicator = new TtmLrc(20);
|
||||
var now = DateTime.UtcNow;
|
||||
var rng = new Random(42);
|
||||
var bars = new GBM(seed: 42).Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
indicator.Update(new TValue(now.AddMinutes(i), 100 + rng.NextDouble() * 50), isNew: true);
|
||||
indicator.Update(bars.Close[i], isNew: true);
|
||||
}
|
||||
|
||||
Assert.True(indicator.RSquared < 1.0, $"R² should be less than 1.0 for random data, got {indicator.RSquared}");
|
||||
@@ -265,12 +261,11 @@ public class TtmLrcTests
|
||||
public void RSquared_ClampedBetweenZeroAndOne()
|
||||
{
|
||||
var indicator = new TtmLrc(5);
|
||||
var now = DateTime.UtcNow;
|
||||
var rng = new Random(123);
|
||||
var bars = new GBM(seed: 123).Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.Update(new TValue(now.AddMinutes(i), 100 + rng.NextDouble() * 100 - 50), isNew: true);
|
||||
indicator.Update(bars.Close[i], isNew: true);
|
||||
Assert.True(indicator.RSquared >= 0.0 && indicator.RSquared <= 1.0, $"R² should be in [0,1], got {indicator.RSquared}");
|
||||
}
|
||||
}
|
||||
@@ -368,23 +363,15 @@ public class TtmLrcTests
|
||||
public void BatchVsStreaming_SameResults()
|
||||
{
|
||||
var streamingIndicator = new TtmLrc(20);
|
||||
var now = DateTime.UtcNow;
|
||||
var rng = new Random(42);
|
||||
int count = 50;
|
||||
|
||||
var times = new List<long>(count);
|
||||
var values = new List<double>(count);
|
||||
var bars = new GBM(seed: 42).Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
long t = (now.AddMinutes(i)).Ticks;
|
||||
double v = 100 + rng.NextDouble() * 20;
|
||||
times.Add(t);
|
||||
values.Add(v);
|
||||
streamingIndicator.Update(new TValue(new DateTime(t, DateTimeKind.Utc), v), isNew: true);
|
||||
streamingIndicator.Update(bars.Close[i], isNew: true);
|
||||
}
|
||||
|
||||
var source = new TSeries(times, values);
|
||||
var source = bars.Close;
|
||||
var (bMid, bU1, bL1, bU2, bL2) = TtmLrc.Batch(source, 20);
|
||||
|
||||
// Compare streaming final values to batch final values
|
||||
@@ -399,20 +386,10 @@ public class TtmLrcTests
|
||||
public void Update_TSeries_ReturnsAllFiveBands()
|
||||
{
|
||||
var indicator = new TtmLrc(10);
|
||||
var now = DateTime.UtcNow;
|
||||
var rng = new Random(42);
|
||||
int count = 20;
|
||||
var bars = new GBM(seed: 42).Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var times = new List<long>(count);
|
||||
var values = new List<double>(count);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
times.Add(now.AddMinutes(i).Ticks);
|
||||
values.Add(100 + rng.NextDouble() * 10);
|
||||
}
|
||||
|
||||
var source = new TSeries(times, values);
|
||||
var source = bars.Close;
|
||||
var (mid, u1, l1, u2, l2) = indicator.Update(source);
|
||||
|
||||
Assert.Equal(count, mid.Count);
|
||||
@@ -425,20 +402,10 @@ public class TtmLrcTests
|
||||
[Fact]
|
||||
public void Calculate_ReturnsIndicatorAndResults()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var rng = new Random(42);
|
||||
int count = 30;
|
||||
var bars = new GBM(seed: 42).Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var times = new List<long>(count);
|
||||
var values = new List<double>(count);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
times.Add(now.AddMinutes(i).Ticks);
|
||||
values.Add(100 + rng.NextDouble() * 15);
|
||||
}
|
||||
|
||||
var source = new TSeries(times, values);
|
||||
var source = bars.Close;
|
||||
var (results, indicator) = TtmLrc.Calculate(source, 15);
|
||||
|
||||
Assert.NotNull(indicator);
|
||||
@@ -674,12 +641,11 @@ public class TtmLrcTests
|
||||
public void LargePeriod_HandlesCorrectly()
|
||||
{
|
||||
var indicator = new TtmLrc(200);
|
||||
var now = DateTime.UtcNow;
|
||||
var rng = new Random(42);
|
||||
var bars = new GBM(seed: 42).Fetch(250, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < 250; i++)
|
||||
{
|
||||
indicator.Update(new TValue(now.AddMinutes(i), 100 + rng.NextDouble() * 50), isNew: true);
|
||||
indicator.Update(bars.Close[i], isNew: true);
|
||||
}
|
||||
|
||||
Assert.True(indicator.IsHot);
|
||||
|
||||
@@ -114,10 +114,10 @@ public sealed class Ubands : AbstractBase
|
||||
_state = default;
|
||||
_p_state = default;
|
||||
_residualBuffer.Clear();
|
||||
Upper = default;
|
||||
Middle = default;
|
||||
Lower = default;
|
||||
Width = default;
|
||||
Upper = new TValue(DateTime.UtcNow, double.NaN);
|
||||
Middle = new TValue(DateTime.UtcNow, double.NaN);
|
||||
Lower = new TValue(DateTime.UtcNow, double.NaN);
|
||||
Width = new TValue(DateTime.UtcNow, double.NaN);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
||||
Reference in New Issue
Block a user