mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 20:48:04 +00:00
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:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user