Refactor error handling and calculations in TheilU, Wmape, and TukeyBiweight classes; update buffer handling for consistency

- Updated buffer handling in TheilU and Wmape classes to ensure consistency after adding new values.
- Changed the resync interval constant in TukeyBiweight for better clarity.
- Refactored state structures to record structs in Gauss, Hann, Hp, Hpf, Kalman, Loess, Notch, and other filter classes for improved performance and readability.
- Enhanced numerical stability in Mama class calculations using Fused Multiply-Add (FMA) for precision.
- Added comprehensive tests for Atan2 validation to compare .NET's Math.Atan2 with PineScript's implementation, ensuring accuracy across various edge cases.
- Updated NDepend badges to reflect changes in classes, methods, and lines of code.
This commit is contained in:
Miha Kralj
2026-01-24 23:07:09 -08:00
parent 744d680435
commit 2836f253c4
53 changed files with 1102 additions and 492 deletions
+3 -4
View File
@@ -60,11 +60,10 @@ public class BbandsIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
var priceSelector = Source.GetPriceSelector();
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
var item = HistoricalData[0, SeekOriginHistory.End];
double price = priceSelector(item);
var time = HistoricalData.Time();
TValue input = new(time, price);
TValue input = new(item.TimeLeft, price);
TValue result = bbands!.Update(input, args.IsNewBar());
MiddleSeries!.SetValue(result.Value, bbands.IsHot, ShowColdValues);
+44 -20
View File
@@ -255,40 +255,64 @@ public sealed class Bbands : AbstractBase
// Calculate SMA using static batch method
Sma.Batch(source, middle, period);
// Calculate standard deviation and bands
for (int i = 0; i < len; i++)
// Calculate standard deviation and bands using O(n) rolling sums
// Instead of O(n²) nested loop, maintain running sum and sumSq
double rollingSum = 0.0;
double rollingSumSq = 0.0;
// Initialize rolling sums for first window
for (int i = 0; i < Math.Min(period, len); i++)
{
double val = source[i];
if (double.IsFinite(val))
{
rollingSum += val;
rollingSumSq += val * val;
}
if (i < period - 1)
{
upper[i] = double.NaN;
lower[i] = double.NaN;
continue;
}
}
// Calculate standard deviation for the current window
double sum = 0.0;
double sumSq = 0.0;
int count = 0;
// Process first complete window
if (len >= period)
{
double mean = rollingSum / period;
double variance = (rollingSumSq / period) - (mean * mean);
variance = Math.Max(0.0, variance); // Guard against negative due to floating point
double stdDev = Math.Sqrt(variance);
double offset = multiplier * stdDev;
upper[period - 1] = middle[period - 1] + offset;
lower[period - 1] = middle[period - 1] - offset;
}
for (int j = i - period + 1; j <= i; j++)
// Process remaining bars with O(1) rolling update
for (int i = period; i < len; i++)
{
// Remove outgoing value (leftmost of previous window)
double outgoing = source[i - period];
if (double.IsFinite(outgoing))
{
double val = source[j];
if (double.IsFinite(val))
{
sum += val;
sumSq += val * val;
count++;
}
rollingSum -= outgoing;
rollingSumSq -= outgoing * outgoing;
}
double variance = 0.0;
if (count > 0)
// Add incoming value (current)
double incoming = source[i];
if (double.IsFinite(incoming))
{
double mean = sum / count;
variance = (sumSq / count) - (mean * mean);
variance = Math.Max(0.0, variance); // Guard against negative due to floating point
rollingSum += incoming;
rollingSumSq += incoming * incoming;
}
// Calculate variance from rolling sums: Var = E[X²] - E[X]²
double mean = rollingSum / period;
double variance = (rollingSumSq / period) - (mean * mean);
variance = Math.Max(0.0, variance); // Guard against negative due to floating point
double stdDev = Math.Sqrt(variance);
double offset = multiplier * stdDev;
+13 -108
View File
@@ -16,14 +16,8 @@ public sealed class Dchannel : ITValuePublisher
private readonly int _period;
private readonly double[] _hBuf;
private readonly double[] _lBuf;
private readonly int[] _hDeque;
private readonly int[] _lDeque;
// Queue state
private int _hHead;
private int _hCount;
private int _lHead;
private int _lCount;
private readonly MonotonicDeque _maxDeque;
private readonly MonotonicDeque _minDeque;
// Rolling counters
private int _count;
@@ -53,12 +47,8 @@ public sealed class Dchannel : ITValuePublisher
_period = period;
_hBuf = new double[_period];
_lBuf = new double[_period];
_hDeque = new int[_period];
_lDeque = new int[_period];
_hHead = 0;
_lHead = 0;
_hCount = 0;
_lCount = 0;
_maxDeque = new MonotonicDeque(_period);
_minDeque = new MonotonicDeque(_period);
_count = 0;
_index = -1;
_state = new State(double.NaN, double.NaN, false);
@@ -96,90 +86,6 @@ public sealed class Dchannel : ITValuePublisher
return (high, low);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void PushMax(long logicalIndex, double value)
{
// Expire old indices
long expire = logicalIndex - _period;
while (_hCount > 0 && _hDeque[_hHead] <= expire)
{
_hHead = (_hHead + 1) % _period;
_hCount--;
}
// Maintain monotonic non-increasing deque
int backIdx;
while (_hCount > 0)
{
backIdx = (_hHead + _hCount - 1) % _period;
int bufIdx = _hDeque[backIdx] % _period;
if (_hBuf[bufIdx] <= value)
{
_hCount--;
}
else
{
break;
}
}
int tail = (_hHead + _hCount) % _period;
_hDeque[tail] = (int)logicalIndex;
_hCount++;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void PushMin(long logicalIndex, double value)
{
long expire = logicalIndex - _period;
while (_lCount > 0 && _lDeque[_lHead] <= expire)
{
_lHead = (_lHead + 1) % _period;
_lCount--;
}
int backIdx;
while (_lCount > 0)
{
backIdx = (_lHead + _lCount - 1) % _period;
int bufIdx = _lDeque[backIdx] % _period;
if (_lBuf[bufIdx] >= value)
{
_lCount--;
}
else
{
break;
}
}
int tail = (_lHead + _lCount) % _period;
_lDeque[tail] = (int)logicalIndex;
_lCount++;
}
private void RebuildDeques()
{
_hHead = 0;
_lHead = 0;
_hCount = 0;
_lCount = 0;
if (_count == 0)
return;
long startLogical = _index - _count + 1;
for (int i = 0; i < _count; i++)
{
long logicalIndex = startLogical + i;
int bufIdx = (int)(logicalIndex % _period);
double h = _hBuf[bufIdx];
double l = _lBuf[bufIdx];
PushMax(logicalIndex, h);
PushMin(logicalIndex, l);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar input, bool isNew = true)
{
@@ -213,17 +119,18 @@ public sealed class Dchannel : ITValuePublisher
if (isNew)
{
PushMax(_index, high);
PushMin(_index, low);
_maxDeque.PushMax(_index, high, _hBuf);
_minDeque.PushMin(_index, low, _lBuf);
}
else
{
// Correcting current bar: rebuild deques to maintain consistency
RebuildDeques();
_maxDeque.RebuildMax(_hBuf, _index, _count);
_minDeque.RebuildMin(_lBuf, _index, _count);
}
double top = _hBuf[_hDeque[_hHead] % _period];
double bot = _lBuf[_lDeque[_lHead] % _period];
double top = _maxDeque.GetExtremum(_hBuf);
double bot = _minDeque.GetExtremum(_lBuf);
double mid = (top + bot) * 0.5;
if (!IsHot && _count >= _period)
@@ -296,10 +203,8 @@ public sealed class Dchannel : ITValuePublisher
{
Array.Clear(_hBuf);
Array.Clear(_lBuf);
_hHead = 0;
_lHead = 0;
_hCount = 0;
_lCount = 0;
_maxDeque.Reset();
_minDeque.Reset();
_count = 0;
_index = -1;
_state = new State(double.NaN, double.NaN, false);
@@ -390,4 +295,4 @@ public sealed class Dchannel : ITValuePublisher
var results = indicator.Update(source);
return (results, indicator);
}
}
}
+13 -109
View File
@@ -1,4 +1,3 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -16,14 +15,8 @@ public sealed class Mmchannel : ITValuePublisher
private readonly int _period;
private readonly double[] _hBuf;
private readonly double[] _lBuf;
private readonly int[] _hDeque;
private readonly int[] _lDeque;
// Queue state
private int _hHead;
private int _hCount;
private int _lHead;
private int _lCount;
private readonly MonotonicDeque _maxDeque;
private readonly MonotonicDeque _minDeque;
// Rolling counters
private int _count;
@@ -53,12 +46,8 @@ public sealed class Mmchannel : ITValuePublisher
_period = period;
_hBuf = new double[_period];
_lBuf = new double[_period];
_hDeque = new int[_period];
_lDeque = new int[_period];
_hHead = 0;
_lHead = 0;
_hCount = 0;
_lCount = 0;
_maxDeque = new MonotonicDeque(_period);
_minDeque = new MonotonicDeque(_period);
_count = 0;
_index = -1;
_state = new State(double.NaN, double.NaN, false);
@@ -96,90 +85,6 @@ public sealed class Mmchannel : ITValuePublisher
return (high, low);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void PushMax(long logicalIndex, double value)
{
// Expire old indices
long expire = logicalIndex - _period;
while (_hCount > 0 && _hDeque[_hHead] <= expire)
{
_hHead = (_hHead + 1) % _period;
_hCount--;
}
// Maintain monotonic non-increasing deque
int backIdx;
while (_hCount > 0)
{
backIdx = (_hHead + _hCount - 1) % _period;
int bufIdx = _hDeque[backIdx] % _period;
if (_hBuf[bufIdx] <= value)
{
_hCount--;
}
else
{
break;
}
}
int tail = (_hHead + _hCount) % _period;
_hDeque[tail] = (int)logicalIndex;
_hCount++;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void PushMin(long logicalIndex, double value)
{
long expire = logicalIndex - _period;
while (_lCount > 0 && _lDeque[_lHead] <= expire)
{
_lHead = (_lHead + 1) % _period;
_lCount--;
}
int backIdx;
while (_lCount > 0)
{
backIdx = (_lHead + _lCount - 1) % _period;
int bufIdx = _lDeque[backIdx] % _period;
if (_lBuf[bufIdx] >= value)
{
_lCount--;
}
else
{
break;
}
}
int tail = (_lHead + _lCount) % _period;
_lDeque[tail] = (int)logicalIndex;
_lCount++;
}
private void RebuildDeques()
{
_hHead = 0;
_lHead = 0;
_hCount = 0;
_lCount = 0;
if (_count == 0)
return;
long startLogical = _index - _count + 1;
for (int i = 0; i < _count; i++)
{
long logicalIndex = startLogical + i;
int bufIdx = (int)(logicalIndex % _period);
double h = _hBuf[bufIdx];
double l = _lBuf[bufIdx];
PushMax(logicalIndex, h);
PushMin(logicalIndex, l);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar input, bool isNew = true)
{
@@ -213,17 +118,18 @@ public sealed class Mmchannel : ITValuePublisher
if (isNew)
{
PushMax(_index, high);
PushMin(_index, low);
_maxDeque.PushMax(_index, high, _hBuf);
_minDeque.PushMin(_index, low, _lBuf);
}
else
{
// Correcting current bar: rebuild deques to maintain consistency
RebuildDeques();
_maxDeque.RebuildMax(_hBuf, _index, _count);
_minDeque.RebuildMin(_lBuf, _index, _count);
}
double top = _hBuf[_hDeque[_hHead] % _period];
double bot = _lBuf[_lDeque[_lHead] % _period];
double top = _maxDeque.GetExtremum(_hBuf);
double bot = _minDeque.GetExtremum(_lBuf);
if (!IsHot && _count >= _period)
_state = _state with { IsHot = true };
@@ -290,10 +196,8 @@ public sealed class Mmchannel : ITValuePublisher
{
Array.Clear(_hBuf);
Array.Clear(_lBuf);
_hHead = 0;
_lHead = 0;
_hCount = 0;
_lCount = 0;
_maxDeque.Reset();
_minDeque.Reset();
_count = 0;
_index = -1;
_state = new State(double.NaN, double.NaN, false);
@@ -357,4 +261,4 @@ public sealed class Mmchannel : ITValuePublisher
var results = indicator.Update(source);
return (results, indicator);
}
}
}
@@ -247,6 +247,44 @@ public class RegchannelTests
Assert.True(double.IsFinite(ind.Lower.Value));
}
[Fact]
public void BarCorrection_UpdatesLastValid()
{
// Verifies that bar correction (isNew:false) with a finite value updates LastValid,
// so subsequent NaN/Inf inputs use the corrected value, not the pre-correction value.
var ind = new Regchannel(5, 2.0);
var now = DateTime.UtcNow;
// Feed initial values
ind.Update(new TValue(now, 100));
ind.Update(new TValue(now, 110));
ind.Update(new TValue(now, 120));
// Last bar: 130 (LastValid should be 130)
ind.Update(new TValue(now, 130));
// Correct the last bar with isNew:false to 140 (should update LastValid to 140)
ind.Update(new TValue(now, 140), isNew: false);
// Now send NaN - it should use LastValid=140, not the old 130
var resultWithNaN = ind.Update(new TValue(now, double.NaN));
// The regression should include 100, 110, 120, 140 (the corrected value)
// If bug existed, it would use 130 instead
Assert.True(double.IsFinite(resultWithNaN.Value));
// Verify by checking the buffer contains the corrected value
// The regression endpoint should reflect using 140 not 130
// For 4 values [100, 110, 120, 140]:
// sumX = 0+1+2+3 = 6, sumX² = 14, n=4
// sumY = 470, sumXY = 0*100 + 1*110 + 2*120 + 3*140 = 770
// denom = 4*14 - 36 = 20
// slope = (4*770 - 6*470) / 20 = (3080 - 2820) / 20 = 13
// intercept = (470 - 13*6) / 4 = (470 - 78) / 4 = 98
// regression at x=3: 98 + 13*3 = 137
Assert.Equal(137.0, resultWithNaN.Value, 1e-9);
}
[Fact]
public void Reset_ClearsState()
{
+2 -2
View File
@@ -135,8 +135,8 @@ public sealed class Regchannel : ITValuePublisher
{
if (double.IsFinite(value))
{
if (isNew)
_state = _state with { LastValid = value };
// Always update LastValid on finite input (including bar corrections)
_state = _state with { LastValid = value };
return value;
}
return double.IsFinite(_state.LastValid) ? _state.LastValid : 0.0;
+38
View File
@@ -270,6 +270,44 @@ public class SdchannelTests
Assert.True(double.IsFinite(result2.Value));
}
[Fact]
public void Sdchannel_BarCorrection_UpdatesLastValid()
{
// Verifies that bar correction (isNew:false) with a finite value updates LastValid,
// so subsequent NaN/Inf inputs use the corrected value, not the pre-correction value.
var s = new Sdchannel(5, 2.0);
var now = DateTime.UtcNow;
// Feed initial values
s.Update(new TValue(now, 100));
s.Update(new TValue(now, 110));
s.Update(new TValue(now, 120));
// Last bar: 130 (LastValid should be 130)
s.Update(new TValue(now, 130));
// Correct the last bar with isNew:false to 140 (should update LastValid to 140)
s.Update(new TValue(now, 140), isNew: false);
// Now send NaN - it should use LastValid=140, not the old 130
var resultWithNaN = s.Update(new TValue(now, double.NaN));
// The regression should include 100, 110, 120, 140 (the corrected value)
// If bug existed, it would use 130 instead
Assert.True(double.IsFinite(resultWithNaN.Value));
// Verify by checking the buffer contains the corrected value
// The regression endpoint should reflect using 140 not 130
// For 4 values [100, 110, 120, 140]:
// sumX = 0+1+2+3 = 6, sumX² = 14, n=4
// sumY = 470, sumXY = 0*100 + 1*110 + 2*120 + 3*140 = 770
// denom = 4*14 - 36 = 20
// slope = (4*770 - 6*470) / 20 = (3080 - 2820) / 20 = 13
// intercept = (470 - 13*6) / 4 = (470 - 78) / 4 = 98
// regression at x=3: 98 + 13*3 = 137
Assert.Equal(137.0, resultWithNaN.Value, 1e-9);
}
[Fact]
public void Sdchannel_Reset_Clears()
{
+2 -2
View File
@@ -135,8 +135,8 @@ public sealed class Sdchannel : ITValuePublisher
{
if (double.IsFinite(value))
{
if (isNew)
_state = _state with { LastValid = value };
// Always update LastValid on finite input (including bar corrections)
_state = _state with { LastValid = value };
return value;
}
return double.IsFinite(_state.LastValid) ? _state.LastValid : 0.0;
+2 -3
View File
@@ -53,11 +53,10 @@ public class StbandsIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
var time = HistoricalData.Time();
var item = HistoricalData[0, SeekOriginHistory.End];
TBar bar = new(
time,
item.TimeLeft,
item[PriceType.Open],
item[PriceType.High],
item[PriceType.Low],
+2 -8
View File
@@ -33,8 +33,6 @@ public sealed class Stbands : AbstractBase
{
private readonly double _multiplier;
private readonly RingBuffer _trBuffer;
private double _trSum;
private int _trCount;
private const int DefaultPeriod = 10;
private const double DefaultMultiplier = 3.0;
private const double MinMultiplier = 0.001;
@@ -47,8 +45,6 @@ public sealed class Stbands : AbstractBase
double FinalLower,
int Trend,
double PrevClose,
double TrSum,
int TrCount,
bool IsInitialized);
private State _state;
@@ -102,9 +98,7 @@ public sealed class Stbands : AbstractBase
private void Init()
{
_index = 0;
_trSum = 0;
_trCount = 0;
_state = new State(0, 0, 1, 0, 0, 0, false);
_state = new State(0, 0, 1, 0, false);
_p_state = _state;
_trBuffer.Clear();
Upper = new TValue(DateTime.UtcNow, 0);
@@ -189,7 +183,7 @@ public sealed class Stbands : AbstractBase
}
// Update state
_state = new State(finalUpper, finalLower, trend, close, _trSum, _trCount, true);
_state = new State(finalUpper, finalLower, trend, close, true);
// Update output values
Upper = new TValue(input.Time, finalUpper);
+2 -3
View File
@@ -57,11 +57,10 @@ public class UbandsIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
var priceSelector = Source.GetPriceSelector();
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
var item = HistoricalData[0, SeekOriginHistory.End];
double price = priceSelector(item);
var time = HistoricalData.Time();
TValue input = new(time, price);
TValue input = new(item.TimeLeft, price);
TValue result = ubands!.Update(input, args.IsNewBar());
MiddleSeries!.SetValue(result.Value, ubands.IsHot, ShowColdValues);
+2 -3
View File
@@ -59,14 +59,13 @@ public class UchannelIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
var item = HistoricalData[0, SeekOriginHistory.End];
double open = item[PriceType.Open];
double high = item[PriceType.High];
double low = item[PriceType.Low];
double close = item[PriceType.Close];
var time = HistoricalData.Time();
TBar input = new(time, open, high, low, close, item[PriceType.Volume]);
TBar input = new(item.TimeLeft, open, high, low, close, item[PriceType.Volume]);
TValue result = uchannel!.Update(input, args.IsNewBar());
MiddleSeries!.SetValue(result.Value, uchannel.IsHot, ShowColdValues);
@@ -58,8 +58,7 @@ public class VwapbandsIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
var time = HistoricalData.Time();
var item = HistoricalData[0, SeekOriginHistory.End];
// VWAP requires OHLCV data - using HLC3 for price
double high = item[PriceType.High];
@@ -67,7 +66,7 @@ public class VwapbandsIndicator : Indicator, IWatchlistIndicator
double close = item[PriceType.Close];
double volume = item[PriceType.Volume];
TBar bar = new(time, item[PriceType.Open], high, low, close, volume);
TBar bar = new(item.TimeLeft, item[PriceType.Open], high, low, close, volume);
TValue result = vwapbands!.Update(bar, args.IsNewBar());
VwapSeries!.SetValue(result.Value, vwapbands.IsHot, ShowColdValues);
+31
View File
@@ -313,6 +313,37 @@ public class VwapbandsTests
Assert.NotEqual(vwapBeforeReset, vwapbands.Vwap.Value);
}
[Fact]
public void Vwapbands_SessionReset_ResetsIsHotGating()
{
var vwapbands = new Vwapbands(1.0);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
var bars = gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Process bars until IsHot is true (WarmupPeriod = 2)
vwapbands.Update(bars[0]);
Assert.False(vwapbands.IsHot);
vwapbands.Update(bars[1]);
Assert.True(vwapbands.IsHot);
// Process more bars to ensure we're well past warmup
for (int i = 2; i < 10; i++)
{
vwapbands.Update(bars[i]);
}
Assert.True(vwapbands.IsHot);
// Reset session - IsHot should become false
var resetBar1 = new TBar(DateTime.UtcNow, 200, 210, 190, 200, 1000);
vwapbands.Update(resetBar1, isNew: true, reset: true);
Assert.False(vwapbands.IsHot, "IsHot should be false after reset (1 bar accumulated)");
// Process second bar after reset - IsHot should become true
var resetBar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 205, 215, 195, 205, 1100);
vwapbands.Update(resetBar2, isNew: true);
Assert.True(vwapbands.IsHot, "IsHot should be true after 2 bars accumulated post-reset");
}
[Fact]
public void Vwapbands_VwapFormula_MatchesExpected()
{
@@ -110,12 +110,12 @@ public sealed class VwapbandsValidationTests : IDisposable
streamingLower2.Add(streamingVwapbands.Lower2.Value);
}
// Span mode - using HLC3 for price
// Span mode - using HLC3 for price (use bar.HLC3 property for consistency)
double[] price = new double[bars.Count];
double[] volume = new double[bars.Count];
for (int i = 0; i < bars.Count; i++)
{
price[i] = (bars[i].High + bars[i].Low + bars[i].Close) / 3.0;
price[i] = bars[i].HLC3;
volume[i] = bars[i].Volume;
}
+4 -2
View File
@@ -138,8 +138,7 @@ public sealed class Vwapbands : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar bar, bool isNew = true, bool reset = false)
{
double hlc3 = (bar.High + bar.Low + bar.Close) / 3.0;
return Update(new TValue(bar.Time, hlc3), bar.Volume, isNew, reset);
return Update(new TValue(bar.Time, bar.HLC3), bar.Volume, isNew, reset);
}
/// <summary>
@@ -174,6 +173,9 @@ public sealed class Vwapbands : AbstractBase
// Handle reset
if (reset || !_state.IsInitialized)
{
// Reset warmup tracking for proper IsHot gating after session reset
_index = 1;
if (vol > 0)
{
_state = _state with
+2 -3
View File
@@ -52,8 +52,7 @@ public class VwapsdIndicator : Indicator, IWatchlistIndicator
protected override void OnUpdate(UpdateArgs args)
{
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
var time = HistoricalData.Time();
var item = HistoricalData[0, SeekOriginHistory.End];
// VWAP requires OHLCV data - using HLC3 for price
double high = item[PriceType.High];
@@ -61,7 +60,7 @@ public class VwapsdIndicator : Indicator, IWatchlistIndicator
double close = item[PriceType.Close];
double volume = item[PriceType.Volume];
TBar bar = new(time, item[PriceType.Open], high, low, close, volume);
TBar bar = new(item.TimeLeft, item[PriceType.Open], high, low, close, volume);
TValue result = vwapsd!.Update(bar, args.IsNewBar());
VwapSeries!.SetValue(result.Value, vwapsd.IsHot, ShowColdValues);
+31
View File
@@ -325,6 +325,37 @@ public class VwapsdTests
Assert.NotEqual(vwapBeforeReset, vwapsd.Vwap.Value);
}
[Fact]
public void Vwapsd_SessionReset_ResetsIsHotGating()
{
var vwapsd = new Vwapsd(1.0);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
var bars = gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Process bars until IsHot is true (WarmupPeriod = 2)
vwapsd.Update(bars[0]);
Assert.False(vwapsd.IsHot);
vwapsd.Update(bars[1]);
Assert.True(vwapsd.IsHot);
// Process more bars to ensure we're well past warmup
for (int i = 2; i < 10; i++)
{
vwapsd.Update(bars[i]);
}
Assert.True(vwapsd.IsHot);
// Reset session - IsHot should become false
var resetBar1 = new TBar(DateTime.UtcNow, 200, 210, 190, 200, 1000);
vwapsd.Update(resetBar1, isNew: true, reset: true);
Assert.False(vwapsd.IsHot, "IsHot should be false after reset (1 bar accumulated)");
// Process second bar after reset - IsHot should become true
var resetBar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 205, 215, 195, 205, 1100);
vwapsd.Update(resetBar2, isNew: true);
Assert.True(vwapsd.IsHot, "IsHot should be true after 2 bars accumulated post-reset");
}
[Fact]
public void Vwapsd_VwapFormula_MatchesExpected()
{
@@ -106,12 +106,12 @@ public sealed class VwapsdValidationTests : IDisposable
streamingLower.Add(streamingVwapsd.Lower.Value);
}
// Span mode - using HLC3 for price
// Span mode - using bar.HLC3 for price to match streaming mode
double[] price = new double[bars.Count];
double[] volume = new double[bars.Count];
for (int i = 0; i < bars.Count; i++)
{
price[i] = (bars[i].High + bars[i].Low + bars[i].Close) / 3.0;
price[i] = bars[i].HLC3;
volume[i] = bars[i].Volume;
}
+4 -2
View File
@@ -132,8 +132,7 @@ public sealed class Vwapsd : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar bar, bool isNew = true, bool reset = false)
{
double hlc3 = (bar.High + bar.Low + bar.Close) / 3.0;
return Update(new TValue(bar.Time, hlc3), bar.Volume, isNew, reset);
return Update(new TValue(bar.Time, bar.HLC3), bar.Volume, isNew, reset);
}
/// <summary>
@@ -168,6 +167,9 @@ public sealed class Vwapsd : AbstractBase
// Handle reset
if (reset || !_state.IsInitialized)
{
// Reset warmup tracking for proper IsHot gating after session reset
_index = 1;
if (vol > 0)
{
_state = _state with