mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-19 02:58:05 +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
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// A monotonic deque for O(1) amortized sliding window min/max queries.
|
||||
/// Maintains elements in strictly monotonic order (non-increasing for max, non-decreasing for min).
|
||||
/// Used by channel indicators (Donchian, MinMax) for efficient rolling extrema.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Algorithm: For each new element, expire indices outside the window, then pop elements
|
||||
/// from the back that would violate monotonicity, then push the new index.
|
||||
/// Time complexity: O(1) amortized per operation (each element pushed/popped at most once).
|
||||
/// Space complexity: O(period) for the deque array.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class MonotonicDeque
|
||||
{
|
||||
private readonly int[] _deque;
|
||||
private readonly int _period;
|
||||
private int _head;
|
||||
private int _count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current front index (the index of the current extremum).
|
||||
/// </summary>
|
||||
public int FrontIndex => _count > 0 ? _deque[_head] : -1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current element count in the deque.
|
||||
/// </summary>
|
||||
public int Count => _count;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new monotonic deque with the specified period.
|
||||
/// </summary>
|
||||
/// <param name="period">The sliding window size.</param>
|
||||
public MonotonicDeque(int period)
|
||||
{
|
||||
if (period <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0");
|
||||
|
||||
_period = period;
|
||||
_deque = new int[period];
|
||||
_head = 0;
|
||||
_count = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes a value for maximum tracking (maintains non-increasing order).
|
||||
/// Smaller or equal values are removed from the back before pushing.
|
||||
/// </summary>
|
||||
/// <param name="logicalIndex">The logical index of the value (used for expiration).</param>
|
||||
/// <param name="value">The value to push.</param>
|
||||
/// <param name="buffer">The circular buffer containing values (indexed by logicalIndex % period).</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void PushMax(long logicalIndex, double value, double[] buffer)
|
||||
{
|
||||
// Expire old indices outside the window
|
||||
long expire = logicalIndex - _period;
|
||||
while (_count > 0 && _deque[_head] <= expire)
|
||||
{
|
||||
_head = (_head + 1) % _period;
|
||||
_count--;
|
||||
}
|
||||
|
||||
// Pop elements from back that are <= value (maintain non-increasing order)
|
||||
while (_count > 0)
|
||||
{
|
||||
int backIdx = (_head + _count - 1) % _period;
|
||||
int bufIdx = _deque[backIdx] % _period;
|
||||
if (buffer[bufIdx] <= value)
|
||||
{
|
||||
_count--;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Push new index
|
||||
int tail = (_head + _count) % _period;
|
||||
_deque[tail] = (int)logicalIndex;
|
||||
_count++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes a value for minimum tracking (maintains non-decreasing order).
|
||||
/// Larger or equal values are removed from the back before pushing.
|
||||
/// </summary>
|
||||
/// <param name="logicalIndex">The logical index of the value (used for expiration).</param>
|
||||
/// <param name="value">The value to push.</param>
|
||||
/// <param name="buffer">The circular buffer containing values (indexed by logicalIndex % period).</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void PushMin(long logicalIndex, double value, double[] buffer)
|
||||
{
|
||||
// Expire old indices outside the window
|
||||
long expire = logicalIndex - _period;
|
||||
while (_count > 0 && _deque[_head] <= expire)
|
||||
{
|
||||
_head = (_head + 1) % _period;
|
||||
_count--;
|
||||
}
|
||||
|
||||
// Pop elements from back that are >= value (maintain non-decreasing order)
|
||||
while (_count > 0)
|
||||
{
|
||||
int backIdx = (_head + _count - 1) % _period;
|
||||
int bufIdx = _deque[backIdx] % _period;
|
||||
if (buffer[bufIdx] >= value)
|
||||
{
|
||||
_count--;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Push new index
|
||||
int tail = (_head + _count) % _period;
|
||||
_deque[tail] = (int)logicalIndex;
|
||||
_count++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current extremum value from the buffer.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The circular buffer containing values.</param>
|
||||
/// <returns>The value at the front of the deque, or NaN if empty.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public double GetExtremum(double[] buffer)
|
||||
{
|
||||
return _count > 0 ? buffer[_deque[_head] % _period] : double.NaN;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the deque to empty state.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset()
|
||||
{
|
||||
_head = 0;
|
||||
_count = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the max deque from scratch using the buffer contents.
|
||||
/// Used after bar corrections (isNew=false) to maintain consistency.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The circular buffer containing values.</param>
|
||||
/// <param name="currentIndex">The current logical index.</param>
|
||||
/// <param name="count">The number of valid elements in the buffer.</param>
|
||||
public void RebuildMax(double[] buffer, long currentIndex, int count)
|
||||
{
|
||||
Reset();
|
||||
if (count == 0) return;
|
||||
|
||||
long startLogical = currentIndex - count + 1;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
long logicalIndex = startLogical + i;
|
||||
int bufIdx = (int)(logicalIndex % _period);
|
||||
PushMax(logicalIndex, buffer[bufIdx], buffer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the min deque from scratch using the buffer contents.
|
||||
/// Used after bar corrections (isNew=false) to maintain consistency.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The circular buffer containing values.</param>
|
||||
/// <param name="currentIndex">The current logical index.</param>
|
||||
/// <param name="count">The number of valid elements in the buffer.</param>
|
||||
public void RebuildMin(double[] buffer, long currentIndex, int count)
|
||||
{
|
||||
Reset();
|
||||
if (count == 0) return;
|
||||
|
||||
long startLogical = currentIndex - count + 1;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
long logicalIndex = startLogical + i;
|
||||
int bufIdx = (int)(logicalIndex % _period);
|
||||
PushMin(logicalIndex, buffer[bufIdx], buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,13 @@ public sealed class RingBuffer : IEnumerable<double>
|
||||
private double _savedSum;
|
||||
private double _savedValue;
|
||||
|
||||
/// <summary>
|
||||
/// Immutable snapshot token for multi-buffer scenarios.
|
||||
/// Allows capturing and restoring buffer state without using the built-in single snapshot.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
public readonly record struct SnapshotToken(int Head, int Count, double Sum, double Value);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new RingBuffer with the specified capacity.
|
||||
/// Uses pinned memory for SIMD compatibility.
|
||||
@@ -241,9 +248,7 @@ public sealed class RingBuffer : IEnumerable<double>
|
||||
private double GetAt(Index index)
|
||||
{
|
||||
int actualIndex = index.IsFromEnd ? _count - index.Value : index.Value;
|
||||
#pragma warning disable S3236 // Caller information arguments should not be provided explicitly - intentionally using cleaner parameter name
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)actualIndex, (uint)_count, nameof(index));
|
||||
#pragma warning restore S3236
|
||||
|
||||
int start = _count == Capacity ? _head : 0;
|
||||
int bufferIdx = (start + actualIndex) % Capacity;
|
||||
@@ -254,9 +259,7 @@ public sealed class RingBuffer : IEnumerable<double>
|
||||
private void SetAt(Index index, double value)
|
||||
{
|
||||
int actualIndex = index.IsFromEnd ? _count - index.Value : index.Value;
|
||||
#pragma warning disable S3236 // Caller information arguments should not be provided explicitly - intentionally using cleaner parameter name
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)actualIndex, (uint)_count, nameof(index));
|
||||
#pragma warning restore S3236
|
||||
|
||||
int start = _count == Capacity ? _head : 0;
|
||||
int bufferIdx = (start + actualIndex) % Capacity;
|
||||
@@ -272,7 +275,7 @@ public sealed class RingBuffer : IEnumerable<double>
|
||||
/// If wrapped, returns span over a copy.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b> Allocation Warning:</b> When the buffer wraps around (i.e., when data spans
|
||||
/// <para><b> Allocation Warning:</b> When the buffer wraps around (i.e., when data spans
|
||||
/// from the end of the internal array back to the beginning), this method allocates a new
|
||||
/// array via <see cref="ToArray"/> to return contiguous data. For allocation-free iteration
|
||||
/// over wrapped buffers, use <see cref="GetSequencedSpans"/> instead.</para>
|
||||
@@ -569,6 +572,32 @@ public sealed class RingBuffer : IEnumerable<double>
|
||||
_buffer[_head] = _savedValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a snapshot token that captures the current buffer state.
|
||||
/// Use this for multi-buffer scenarios where you need to snapshot multiple buffers atomically.
|
||||
/// Must be called BEFORE adding a new value if you intend to restore later.
|
||||
/// </summary>
|
||||
/// <returns>An immutable token containing the buffer state.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public SnapshotToken GetSnapshot()
|
||||
{
|
||||
return new SnapshotToken(_head, _count, _sum, _buffer[_head]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the buffer to the state captured in the provided snapshot token.
|
||||
/// Use this for multi-buffer scenarios where you need to restore multiple buffers atomically.
|
||||
/// </summary>
|
||||
/// <param name="token">The snapshot token to restore from.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RestoreSnapshot(SnapshotToken token)
|
||||
{
|
||||
_head = token.Head;
|
||||
_count = token.Count;
|
||||
_sum = token.Sum;
|
||||
_buffer[_head] = token.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an enumerator that iterates through the buffer in chronological order.
|
||||
/// </summary>
|
||||
|
||||
@@ -14,6 +14,18 @@ namespace QuanTAlib;
|
||||
/// </summary>
|
||||
public static class ErrorHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Default stack allocation threshold for temporary buffers.
|
||||
/// 256 doubles = 2KB, safe margin for nested calls on 1MB thread stack.
|
||||
/// Beyond this threshold, ArrayPool is used instead of stackalloc.
|
||||
/// </summary>
|
||||
public const int StackAllocThreshold = 256;
|
||||
|
||||
/// <summary>
|
||||
/// Default resync interval for running sums to correct floating-point drift.
|
||||
/// </summary>
|
||||
public const int DefaultResyncInterval = 1000;
|
||||
|
||||
private const string SpanLengthMismatchMessage = "All spans must have the same length";
|
||||
|
||||
/// <summary>
|
||||
@@ -434,7 +446,6 @@ public static class ErrorHelpers
|
||||
if (len == 0)
|
||||
return;
|
||||
|
||||
const int StackAllocThreshold = 256;
|
||||
double[]? rented = null;
|
||||
|
||||
#pragma warning disable S1121 // Assignments should not be made from within sub-expressions
|
||||
@@ -508,7 +519,6 @@ public static class ErrorHelpers
|
||||
if (len == 0)
|
||||
return;
|
||||
|
||||
const int StackAllocThreshold = 256;
|
||||
double[]? rented = null;
|
||||
|
||||
#pragma warning disable S1121 // Assignments should not be made from within sub-expressions
|
||||
@@ -584,7 +594,6 @@ public static class ErrorHelpers
|
||||
if (len == 0)
|
||||
return;
|
||||
|
||||
const int StackAllocThreshold = 256;
|
||||
double[]? rentedErrors = null;
|
||||
double[]? rentedWeights = null;
|
||||
|
||||
@@ -661,10 +670,49 @@ public static class ErrorHelpers
|
||||
}
|
||||
}
|
||||
|
||||
#region Private Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes input spans by replacing NaN/Infinity values with the last valid value.
|
||||
/// Writes sanitized values to output spans for use in batch calculations.
|
||||
/// </summary>
|
||||
/// <param name="actual">Input actual values</param>
|
||||
/// <param name="predicted">Input predicted values</param>
|
||||
/// <param name="actualOut">Output sanitized actual values</param>
|
||||
/// <param name="predictedOut">Output sanitized predicted values</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double FindFirstValidValue(ReadOnlySpan<double> span)
|
||||
public static void SanitizeInputs(
|
||||
ReadOnlySpan<double> actual,
|
||||
ReadOnlySpan<double> predicted,
|
||||
Span<double> actualOut,
|
||||
Span<double> predictedOut)
|
||||
{
|
||||
if (actual.Length != predicted.Length || actual.Length != actualOut.Length || actual.Length != predictedOut.Length)
|
||||
throw new ArgumentException(SpanLengthMismatchMessage, nameof(predictedOut));
|
||||
|
||||
int len = actual.Length;
|
||||
if (len == 0)
|
||||
return;
|
||||
|
||||
double lastValidActual = FindFirstValidValue(actual);
|
||||
double lastValidPredicted = FindFirstValidValue(predicted);
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double act = actual[i];
|
||||
double pred = predicted[i];
|
||||
|
||||
if (double.IsFinite(act)) lastValidActual = act; else act = lastValidActual;
|
||||
if (double.IsFinite(pred)) lastValidPredicted = pred; else pred = lastValidPredicted;
|
||||
|
||||
actualOut[i] = act;
|
||||
predictedOut[i] = pred;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the first finite value in a span, or returns 0.0 if none found.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double FindFirstValidValue(ReadOnlySpan<double> span)
|
||||
{
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
@@ -674,6 +722,8 @@ public static class ErrorHelpers
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
#region Private Helpers
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsDataClean(ReadOnlySpan<double> actual, ReadOnlySpan<double> predicted)
|
||||
{
|
||||
|
||||
+105
-116
@@ -23,8 +23,7 @@ public sealed class Stc : AbstractBase
|
||||
private bool _isNew;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
#pragma warning disable CA1066 // Implement IEquatable<T> because it overrides Equals
|
||||
private struct State
|
||||
private record struct State
|
||||
{
|
||||
public double FastEma;
|
||||
public double SlowEma;
|
||||
@@ -39,7 +38,6 @@ public sealed class Stc : AbstractBase
|
||||
public double Stoch1Min;
|
||||
public double Stoch1Max;
|
||||
}
|
||||
#pragma warning restore CA1066
|
||||
|
||||
private State _s, _ps;
|
||||
private int _samples;
|
||||
@@ -119,84 +117,111 @@ public sealed class Stc : AbstractBase
|
||||
return Math.Clamp(x, 0, 100);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies final smoothing to stoch2Raw based on smoothing mode.
|
||||
/// Shared between Update() and Calculate() to eliminate duplication.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double ApplySmoothing(double stoch2Raw, StcSmoothing smoothing, double dAlpha, ref double stoch2Ema, ref double prevStc)
|
||||
{
|
||||
double stc;
|
||||
switch (smoothing)
|
||||
{
|
||||
case StcSmoothing.Ema:
|
||||
stoch2Ema = double.IsNaN(stoch2Ema)
|
||||
? stoch2Raw
|
||||
: Math.FusedMultiplyAdd(dAlpha, stoch2Raw - stoch2Ema, stoch2Ema);
|
||||
stc = Clamp100(stoch2Ema);
|
||||
break;
|
||||
|
||||
case StcSmoothing.Sigmoid:
|
||||
stc = 100.0 / (1.0 + Math.Exp(-0.1 * (stoch2Raw - 50.0)));
|
||||
break;
|
||||
|
||||
case StcSmoothing.Digital:
|
||||
if (stoch2Raw > 75) stc = 100;
|
||||
else if (stoch2Raw < 25) stc = 0;
|
||||
else stc = double.IsNaN(prevStc) ? stoch2Raw : prevStc;
|
||||
break;
|
||||
|
||||
default: // Includes StcSmoothing.None
|
||||
stc = stoch2Raw;
|
||||
break;
|
||||
}
|
||||
prevStc = stc;
|
||||
return stc;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates min/max tracking for a sliding window.
|
||||
/// Returns true if a full rescan is needed (removed value was at boundary).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool UpdateMinMaxCore(double added, double removed, bool hasRemoved, ref double min, ref double max)
|
||||
{
|
||||
if (double.IsNaN(added)) return false;
|
||||
|
||||
bool expandMin = added < min;
|
||||
bool expandMax = added > max;
|
||||
|
||||
if (!hasRemoved)
|
||||
{
|
||||
if (expandMin) min = added;
|
||||
if (expandMax) max = added;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use relative tolerance for floating-point comparison
|
||||
double tolerance = Math.Max(Math.Abs(min), Math.Abs(max)) * 1e-12;
|
||||
if (tolerance < 1e-15) tolerance = 1e-15; // minimum absolute tolerance
|
||||
bool removedMin = Math.Abs(removed - min) <= tolerance;
|
||||
bool removedMax = Math.Abs(removed - max) <= tolerance;
|
||||
|
||||
if (expandMin) min = added;
|
||||
if (expandMax) max = added;
|
||||
|
||||
return (removedMin && !expandMin) || (removedMax && !expandMax);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rescans a span to find new min/max values.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void RescanMinMax(ReadOnlySpan<double> span, ref double min, ref double max)
|
||||
{
|
||||
min = double.PositiveInfinity;
|
||||
max = double.NegativeInfinity;
|
||||
foreach (double v in span)
|
||||
{
|
||||
if (double.IsNaN(v)) continue;
|
||||
if (v < min) min = v;
|
||||
if (v > max) max = v;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void UpdateMinMax(double added, double removed, bool hasRemoved, RingBuffer buf, ref double min, ref double max)
|
||||
{
|
||||
if (double.IsNaN(added)) return;
|
||||
|
||||
bool expandMin = added < min;
|
||||
bool expandMax = added > max;
|
||||
|
||||
if (!hasRemoved)
|
||||
{
|
||||
if (expandMin) min = added;
|
||||
if (expandMax) max = added;
|
||||
return;
|
||||
}
|
||||
|
||||
// Use relative tolerance for floating-point comparison
|
||||
double tolerance = Math.Max(Math.Abs(min), Math.Abs(max)) * 1e-12;
|
||||
if (tolerance < 1e-15) tolerance = 1e-15; // minimum absolute tolerance
|
||||
bool removedMin = Math.Abs(removed - min) <= tolerance;
|
||||
bool removedMax = Math.Abs(removed - max) <= tolerance;
|
||||
|
||||
if (expandMin) min = added;
|
||||
if (expandMax) max = added;
|
||||
|
||||
if ((removedMin && !expandMin) || (removedMax && !expandMax))
|
||||
if (UpdateMinMaxCore(added, removed, hasRemoved, ref min, ref max))
|
||||
{
|
||||
var span = buf.IsFull ? buf.InternalBuffer : buf.GetSpan();
|
||||
min = double.PositiveInfinity;
|
||||
max = double.NegativeInfinity;
|
||||
foreach (double v in span)
|
||||
{
|
||||
if (double.IsNaN(v)) continue;
|
||||
if (v < min) min = v;
|
||||
if (v > max) max = v;
|
||||
}
|
||||
RescanMinMax(span, ref min, ref max);
|
||||
}
|
||||
}
|
||||
|
||||
// Overload for Span based buffers (Calculate)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void UpdateMinMax(double added, double removed, bool hasRemoved, ReadOnlySpan<double> buf, ref double min, ref double max)
|
||||
{
|
||||
if (double.IsNaN(added)) return;
|
||||
|
||||
bool expandMin = added < min;
|
||||
bool expandMax = added > max;
|
||||
|
||||
if (!hasRemoved)
|
||||
if (UpdateMinMaxCore(added, removed, hasRemoved, ref min, ref max))
|
||||
{
|
||||
if (expandMin) min = added;
|
||||
if (expandMax) max = added;
|
||||
return;
|
||||
}
|
||||
|
||||
// Use relative tolerance for floating-point comparison
|
||||
double tolerance = Math.Max(Math.Abs(min), Math.Abs(max)) * 1e-12;
|
||||
if (tolerance < 1e-15) tolerance = 1e-15; // minimum absolute tolerance
|
||||
bool removedMin = Math.Abs(removed - min) <= tolerance;
|
||||
bool removedMax = Math.Abs(removed - max) <= tolerance;
|
||||
|
||||
if (expandMin) min = added;
|
||||
if (expandMax) max = added;
|
||||
|
||||
if ((removedMin && !expandMin) || (removedMax && !expandMax))
|
||||
{
|
||||
min = double.PositiveInfinity;
|
||||
max = double.NegativeInfinity;
|
||||
foreach (double v in buf)
|
||||
{
|
||||
if (double.IsNaN(v)) continue;
|
||||
if (v < min) min = v;
|
||||
if (v > max) max = v;
|
||||
}
|
||||
RescanMinMax(buf, ref min, ref max);
|
||||
}
|
||||
}
|
||||
|
||||
// skipcq: CS-R1140 - Cyclomatic complexity justified: STC algorithm requires
|
||||
// sequential MACD→Stoch1→Stoch2→Smoothing pipeline with min/max tracking per stage.
|
||||
// Splitting would fragment the tightly-coupled state machine and harm readability.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
// skipcq: CS-R1140
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
_isNew = isNew;
|
||||
@@ -312,34 +337,7 @@ public sealed class Stc : AbstractBase
|
||||
double stc = double.NaN;
|
||||
if (!double.IsNaN(stoch2Raw))
|
||||
{
|
||||
switch (_smoothing)
|
||||
{
|
||||
case StcSmoothing.Ema:
|
||||
s.Stoch2Ema = double.IsNaN(s.Stoch2Ema)
|
||||
? stoch2Raw
|
||||
: Math.FusedMultiplyAdd(_dAlpha, stoch2Raw - s.Stoch2Ema, s.Stoch2Ema);
|
||||
stc = Clamp100(s.Stoch2Ema);
|
||||
break;
|
||||
|
||||
case StcSmoothing.Sigmoid:
|
||||
stc = 100.0 / (1.0 + Math.Exp(-0.1 * (stoch2Raw - 50.0)));
|
||||
break;
|
||||
|
||||
case StcSmoothing.Digital:
|
||||
if (stoch2Raw > 75) stc = 100;
|
||||
else if (stoch2Raw < 25) stc = 0;
|
||||
else stc = double.IsNaN(s.PrevStc) ? stoch2Raw : s.PrevStc;
|
||||
break;
|
||||
|
||||
case StcSmoothing.None:
|
||||
stc = stoch2Raw;
|
||||
break;
|
||||
|
||||
default:
|
||||
stc = stoch2Raw;
|
||||
break;
|
||||
}
|
||||
s.PrevStc = stc;
|
||||
stc = ApplySmoothing(stoch2Raw, _smoothing, _dAlpha, ref s.Stoch2Ema, ref s.PrevStc);
|
||||
}
|
||||
|
||||
if (isNew) _samples++;
|
||||
@@ -374,7 +372,19 @@ public sealed class Stc : AbstractBase
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
// skipcq: CS-R1140
|
||||
/// <summary>
|
||||
/// Static convenience method that creates a new Stc instance and processes the entire series.
|
||||
/// </summary>
|
||||
public static TSeries Calculate(TSeries source, int kPeriod = 10, int dPeriod = 3, int fastLength = 23, int slowLength = 50, StcSmoothing smoothing = StcSmoothing.Ema)
|
||||
{
|
||||
var indicator = new Stc(kPeriod, dPeriod, fastLength, slowLength, smoothing);
|
||||
return indicator.Update(source);
|
||||
}
|
||||
|
||||
// skipcq: CS-R1140 - Cyclomatic complexity justified: span-based Calculate must
|
||||
// replicate the full STC state machine inline for zero-allocation performance.
|
||||
// The sequential MACD→Stoch1→Stoch2→Smoothing pipeline cannot be decomposed
|
||||
// without introducing heap allocations or sacrificing inlining opportunities.
|
||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> output,
|
||||
int kPeriod = 10, int dPeriod = 3, int fastLength = 23, int slowLength = 50, StcSmoothing smoothing = StcSmoothing.Ema)
|
||||
{
|
||||
@@ -522,28 +532,7 @@ public sealed class Stc : AbstractBase
|
||||
double stc = double.NaN;
|
||||
if (!double.IsNaN(stoch2Raw))
|
||||
{
|
||||
if (smoothing == StcSmoothing.Ema)
|
||||
{
|
||||
stoch2Ema = double.IsNaN(stoch2Ema)
|
||||
? stoch2Raw
|
||||
: Math.FusedMultiplyAdd(dAlpha, stoch2Raw - stoch2Ema, stoch2Ema);
|
||||
stc = Clamp100(stoch2Ema);
|
||||
}
|
||||
else if (smoothing == StcSmoothing.Sigmoid)
|
||||
{
|
||||
stc = 100.0 / (1.0 + Math.Exp(-0.1 * (stoch2Raw - 50.0)));
|
||||
}
|
||||
else if (smoothing == StcSmoothing.Digital)
|
||||
{
|
||||
if (stoch2Raw > 75) stc = 100;
|
||||
else if (stoch2Raw < 25) stc = 0;
|
||||
else stc = double.IsNaN(prevStc) ? stoch2Raw : prevStc;
|
||||
}
|
||||
else
|
||||
{
|
||||
stc = stoch2Raw;
|
||||
}
|
||||
prevStc = stc;
|
||||
stc = ApplySmoothing(stoch2Raw, smoothing, dAlpha, ref stoch2Ema, ref prevStc);
|
||||
}
|
||||
|
||||
output[i] = stc;
|
||||
|
||||
@@ -104,18 +104,15 @@ public sealed class Mase : AbstractBase
|
||||
{
|
||||
_state = _p_state;
|
||||
|
||||
// Incremental update for error buffer: get current newest, compute delta
|
||||
double currentNewestError = _errorBuffer.Count > 0 ? _errorBuffer.Newest : 0.0;
|
||||
double deltaError = absError - currentNewestError;
|
||||
_state.ErrorSum += deltaError;
|
||||
// Bar correction: update buffer and recalculate sums
|
||||
// Note: _p_state was saved BEFORE the Add, but buffer still has the added value
|
||||
// So we update newest and recalculate to ensure consistency
|
||||
_errorBuffer.UpdateNewest(absError);
|
||||
|
||||
// Incremental update for scale buffer: get current newest, compute delta
|
||||
double currentNewestScale = _scaleBuffer.Count > 0 ? _scaleBuffer.Newest : 0.0;
|
||||
double deltaScale = naiveDiff - currentNewestScale;
|
||||
_state.ScaleSum += deltaScale;
|
||||
_scaleBuffer.UpdateNewest(naiveDiff);
|
||||
|
||||
_state.ErrorSum = _errorBuffer.RecalculateSum();
|
||||
_state.ScaleSum = _scaleBuffer.RecalculateSum();
|
||||
|
||||
_state.PrevActual = actualVal;
|
||||
}
|
||||
|
||||
|
||||
@@ -107,9 +107,7 @@ public sealed class Rae : AbstractBase
|
||||
{
|
||||
_state = _p_state;
|
||||
|
||||
// Update actual buffer
|
||||
double removedActual = _actualBuffer.Count == _actualBuffer.Capacity ? _actualBuffer.Oldest : 0.0;
|
||||
_state.ActualSum = _state.ActualSum - removedActual + actualVal;
|
||||
// Update buffers and recalculate sums (buffer state is inconsistent with _p_state)
|
||||
_actualBuffer.UpdateNewest(actualVal);
|
||||
_state.ActualSum = _actualBuffer.RecalculateSum();
|
||||
|
||||
@@ -118,12 +116,9 @@ public sealed class Rae : AbstractBase
|
||||
double absError = Math.Abs(actualVal - predictedVal);
|
||||
double absBaseline = Math.Abs(actualVal - mean);
|
||||
|
||||
// Update error buffer
|
||||
_absErrorBuffer.UpdateNewest(absError);
|
||||
_state.AbsErrorSum = _absErrorBuffer.RecalculateSum();
|
||||
|
||||
// Update baseline buffer
|
||||
_absBaselineBuffer.UpdateNewest(absBaseline);
|
||||
_state.AbsErrorSum = _absErrorBuffer.RecalculateSum();
|
||||
_state.AbsBaselineSum = _absBaselineBuffer.RecalculateSum();
|
||||
}
|
||||
|
||||
@@ -292,4 +287,4 @@ public sealed class Rae : AbstractBase
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,25 +110,22 @@ public sealed class Rsquared : AbstractBase
|
||||
{
|
||||
_state = _p_state;
|
||||
|
||||
// Update actual buffer
|
||||
double removedActual = _actualBuffer.Count == _actualBuffer.Capacity ? _actualBuffer.Oldest : 0.0;
|
||||
_state.ActualSum = _state.ActualSum - removedActual + actualVal;
|
||||
// Bar correction: update buffers and recalculate sums
|
||||
_actualBuffer.UpdateNewest(actualVal);
|
||||
_state.ActualSum = _actualBuffer.RecalculateSum();
|
||||
|
||||
// Calculate mean and errors
|
||||
double mean = _state.ActualSum / _actualBuffer.Count;
|
||||
double mean = _actualBuffer.RecalculateSum() / _actualBuffer.Count;
|
||||
_state.ActualSum = _actualBuffer.RecalculateSum();
|
||||
|
||||
double residual = actualVal - predictedVal;
|
||||
double totalDev = actualVal - mean;
|
||||
double sqResidual = residual * residual;
|
||||
double sqTotal = totalDev * totalDev;
|
||||
|
||||
// Update squared residual buffer
|
||||
_sqResidualBuffer.UpdateNewest(sqResidual);
|
||||
_state.SqResidualSum = _sqResidualBuffer.RecalculateSum();
|
||||
|
||||
// Update squared total buffer
|
||||
_sqTotalBuffer.UpdateNewest(sqTotal);
|
||||
|
||||
_state.SqResidualSum = _sqResidualBuffer.RecalculateSum();
|
||||
_state.SqTotalSum = _sqTotalBuffer.RecalculateSum();
|
||||
}
|
||||
|
||||
|
||||
@@ -124,14 +124,15 @@ public sealed class TheilU : AbstractBase
|
||||
{
|
||||
_state = _p_state;
|
||||
|
||||
// Simplified: just update buffers and recalculate sums (no redundant arithmetic)
|
||||
// Bar correction: update buffer and recalculate sums
|
||||
// Note: _p_state was saved BEFORE the Add, but buffer still has the added value
|
||||
// So we update newest and recalculate to ensure consistency
|
||||
_sqErrorBuffer.UpdateNewest(sqError);
|
||||
_state.SqErrorSum = _sqErrorBuffer.RecalculateSum();
|
||||
|
||||
_sqActualBuffer.UpdateNewest(sqActual);
|
||||
_state.SqActualSum = _sqActualBuffer.RecalculateSum();
|
||||
|
||||
_sqPredBuffer.UpdateNewest(sqPred);
|
||||
|
||||
_state.SqErrorSum = _sqErrorBuffer.RecalculateSum();
|
||||
_state.SqActualSum = _sqActualBuffer.RecalculateSum();
|
||||
_state.SqPredSum = _sqPredBuffer.RecalculateSum();
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ public sealed class TukeyBiweight : BiInputIndicatorBase
|
||||
{
|
||||
private readonly double _cSquaredOver6;
|
||||
private const double DefaultC = 4.685; // 95% efficiency for normal distribution
|
||||
private const int BatchResyncInterval = 1000; // Local constant for static Batch method
|
||||
|
||||
public TukeyBiweight(int period, double c = DefaultC)
|
||||
: base(period, $"TukeyBiweight({period},{c:F3})")
|
||||
@@ -104,7 +105,7 @@ public sealed class TukeyBiweight : BiInputIndicatorBase
|
||||
ErrorHelpers.ComputeTukeyBiweightErrors(actual, predicted, errors, c);
|
||||
|
||||
// Step 2: Apply rolling mean
|
||||
ErrorHelpers.ApplyRollingMean(errors, output, period, ResyncInterval);
|
||||
ErrorHelpers.ApplyRollingMean(errors, output, period, BatchResyncInterval);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -97,11 +97,13 @@ public sealed class Wmape : AbstractBase
|
||||
}
|
||||
else
|
||||
{
|
||||
// Simplified: just update buffers and recalculate sums (no redundant arithmetic)
|
||||
// Bar correction: update buffer and recalculate sums
|
||||
// Note: _p_state was saved BEFORE the Add, but buffer still has the added value
|
||||
// So we update newest and recalculate to ensure consistency
|
||||
_absErrorBuffer.UpdateNewest(absError);
|
||||
_state.AbsErrorSum = _absErrorBuffer.RecalculateSum();
|
||||
|
||||
_absActualBuffer.UpdateNewest(absActual);
|
||||
|
||||
_state.AbsErrorSum = _absErrorBuffer.RecalculateSum();
|
||||
_state.AbsActualSum = _absActualBuffer.RecalculateSum();
|
||||
}
|
||||
|
||||
|
||||
@@ -25,13 +25,11 @@ public sealed class Gauss : AbstractBase
|
||||
private State _p_state;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
#pragma warning disable CA1066 // Implement IEquatable<T> because it overrides Equals
|
||||
private struct State
|
||||
private record struct State
|
||||
{
|
||||
public double LastValue;
|
||||
public bool IsHot;
|
||||
}
|
||||
#pragma warning restore CA1066
|
||||
|
||||
/// <summary>
|
||||
/// Gets the kernel size used for the filter.
|
||||
|
||||
@@ -20,13 +20,11 @@ public sealed class Hann : AbstractBase
|
||||
private State _p_state;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
#pragma warning disable CA1066 // Implement IEquatable<T> because it overrides Equals
|
||||
private struct State
|
||||
private record struct State
|
||||
{
|
||||
public double LastValue;
|
||||
public bool IsHot;
|
||||
}
|
||||
#pragma warning restore CA1066
|
||||
|
||||
/// <summary>
|
||||
/// Gets the length of the window (period).
|
||||
|
||||
@@ -25,15 +25,13 @@ public sealed class Hp : AbstractBase
|
||||
private State _p_state;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
#pragma warning disable CA1066 // Implement IEquatable<T> because it overrides Equals
|
||||
private struct State
|
||||
private record struct State
|
||||
{
|
||||
public double Trend;
|
||||
public double PrevTrend;
|
||||
public double PrevPrice;
|
||||
public bool IsInitialized;
|
||||
}
|
||||
#pragma warning restore CA1066
|
||||
|
||||
/// <summary>
|
||||
/// Smoothing parameter (lambda). Common values: 1600 (Quarterly), 14400 (Monthly), 6.25 (Annual).
|
||||
|
||||
@@ -22,8 +22,7 @@ public sealed class Hpf : AbstractBase
|
||||
private State _pState;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
#pragma warning disable CA1066 // Implement IEquatable<T> because it overrides Equals
|
||||
private struct State
|
||||
private record struct State
|
||||
{
|
||||
public double Hp1;
|
||||
public double Hp2;
|
||||
@@ -32,7 +31,6 @@ public sealed class Hpf : AbstractBase
|
||||
public int Samples;
|
||||
public bool HasSrc;
|
||||
}
|
||||
#pragma warning restore CA1066
|
||||
|
||||
public int Length { get; }
|
||||
|
||||
|
||||
@@ -42,14 +42,12 @@ public sealed class Kalman : AbstractBase
|
||||
private State _pState;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
#pragma warning disable CA1066 // Implement IEquatable<T> because it overrides Equals
|
||||
private struct State
|
||||
private record struct State
|
||||
{
|
||||
public double X;
|
||||
public double P;
|
||||
public int Samples;
|
||||
}
|
||||
#pragma warning restore CA1066
|
||||
|
||||
/// <summary>
|
||||
/// Process noise covariance. Defaults to 0.01.
|
||||
|
||||
@@ -28,14 +28,12 @@ public sealed class Loess : AbstractBase
|
||||
private Snapshot _pSnap;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
#pragma warning disable CA1066 // Implement IEquatable<T> because it overrides Equals
|
||||
private struct Snapshot
|
||||
private record struct Snapshot
|
||||
{
|
||||
public double LastOutput;
|
||||
public double LastFiniteInput;
|
||||
public bool HasFiniteInput;
|
||||
}
|
||||
#pragma warning restore CA1066
|
||||
|
||||
/// <summary>
|
||||
/// Gets the period of the filter.
|
||||
|
||||
@@ -15,13 +15,11 @@ public sealed class Notch : AbstractBase
|
||||
private State _p_state;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
#pragma warning disable CA1066 // Implement IEquatable<T> because it overrides Equals
|
||||
private struct State
|
||||
private record struct State
|
||||
{
|
||||
public double X1, X2, Y1, Y2;
|
||||
public double LastValue;
|
||||
}
|
||||
#pragma warning restore CA1066
|
||||
|
||||
public int NotchFreq { get; }
|
||||
public double Bandwidth { get; }
|
||||
|
||||
@@ -123,7 +123,10 @@ public sealed class Usf : AbstractBase
|
||||
|
||||
double usf = (_state.Count < 4)
|
||||
? val
|
||||
: (1.0 - _c1) * val + (2.0 * _c1 - _c2) * _state.PrevInput1 - (_c1 + _c3) * _state.PrevInput2 + _c2 * _state.Usf1 + _c3 * _state.Usf2;
|
||||
: Math.FusedMultiplyAdd(_c3, _state.Usf2,
|
||||
Math.FusedMultiplyAdd(_c2, _state.Usf1,
|
||||
Math.FusedMultiplyAdd(_k2, _state.PrevInput2,
|
||||
Math.FusedMultiplyAdd(_k1, _state.PrevInput1, _k0 * val))));
|
||||
|
||||
_state.Usf2 = _state.Usf1;
|
||||
_state.Usf1 = usf;
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validates the behavioral differences between .NET's Math.Atan2 and PineScript's custom atan2 implementation.
|
||||
///
|
||||
/// PineScript's atan2 uses a numerically stable algorithm:
|
||||
/// 1. If |x| > |y|: angle = atan(|y|/|x|)
|
||||
/// 2. If |y| >= |x|: angle = π/2 - atan(|x|/|y|)
|
||||
/// 3. Then applies quadrant correction based on sign of x and y
|
||||
///
|
||||
/// Math.Atan2 follows the standard IEEE convention: atan2(y, x) returns the angle
|
||||
/// in radians between the positive x-axis and the point (x, y).
|
||||
///
|
||||
/// Both return values in the range [-π, π], but they can differ in edge cases
|
||||
/// and have different numerical stability characteristics.
|
||||
/// </summary>
|
||||
public class Atan2ValidationTests
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public Atan2ValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PineScript-style atan2 implementation for comparison.
|
||||
/// This is a direct port of the algorithm from ht_phasor.pine.
|
||||
/// </summary>
|
||||
private static double PineScriptAtan2(double y, double x)
|
||||
{
|
||||
if (y == 0.0 && x == 0.0)
|
||||
throw new ArgumentException("atan2: Both y and x cannot be zero", nameof(y));
|
||||
|
||||
double ay = Math.Abs(y);
|
||||
double ax = Math.Abs(x);
|
||||
double angle;
|
||||
|
||||
if (ax > ay)
|
||||
{
|
||||
angle = Math.Atan(ay / ax);
|
||||
}
|
||||
else
|
||||
{
|
||||
angle = (Math.PI / 2.0) - Math.Atan(ax / ay);
|
||||
}
|
||||
|
||||
if (x < 0.0)
|
||||
angle = Math.PI - angle;
|
||||
if (y < 0.0)
|
||||
angle = -angle;
|
||||
|
||||
return angle;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_StandardQuadrant1_BothMatch()
|
||||
{
|
||||
// First quadrant: x > 0, y > 0
|
||||
double y = 1.0, x = 1.0;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
|
||||
_output.WriteLine($"Q1: Math.Atan2({y}, {x}) = {dotNet:F15}");
|
||||
_output.WriteLine($"Q1: PineAtan2({y}, {x}) = {pine:F15}");
|
||||
_output.WriteLine($"Q1: Difference = {Math.Abs(dotNet - pine):E}");
|
||||
|
||||
Assert.Equal(dotNet, pine, precision: 14);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_StandardQuadrant2_BothMatch()
|
||||
{
|
||||
// Second quadrant: x < 0, y > 0
|
||||
double y = 1.0, x = -1.0;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
|
||||
_output.WriteLine($"Q2: Math.Atan2({y}, {x}) = {dotNet:F15}");
|
||||
_output.WriteLine($"Q2: PineAtan2({y}, {x}) = {pine:F15}");
|
||||
_output.WriteLine($"Q2: Difference = {Math.Abs(dotNet - pine):E}");
|
||||
|
||||
Assert.Equal(dotNet, pine, precision: 14);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_StandardQuadrant3_BothMatch()
|
||||
{
|
||||
// Third quadrant: x < 0, y < 0
|
||||
double y = -1.0, x = -1.0;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
|
||||
_output.WriteLine($"Q3: Math.Atan2({y}, {x}) = {dotNet:F15}");
|
||||
_output.WriteLine($"Q3: PineAtan2({y}, {x}) = {pine:F15}");
|
||||
_output.WriteLine($"Q3: Difference = {Math.Abs(dotNet - pine):E}");
|
||||
|
||||
Assert.Equal(dotNet, pine, precision: 14);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_StandardQuadrant4_BothMatch()
|
||||
{
|
||||
// Fourth quadrant: x > 0, y < 0
|
||||
double y = -1.0, x = 1.0;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
|
||||
_output.WriteLine($"Q4: Math.Atan2({y}, {x}) = {dotNet:F15}");
|
||||
_output.WriteLine($"Q4: PineAtan2({y}, {x}) = {pine:F15}");
|
||||
_output.WriteLine($"Q4: Difference = {Math.Abs(dotNet - pine):E}");
|
||||
|
||||
Assert.Equal(dotNet, pine, precision: 14);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_AxisAligned_PositiveY()
|
||||
{
|
||||
// On positive Y-axis: x = 0, y > 0 → π/2
|
||||
double y = 1.0, x = 0.0;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
double expected = Math.PI / 2.0;
|
||||
|
||||
_output.WriteLine($"Positive Y-axis: Math.Atan2({y}, {x}) = {dotNet:F15}");
|
||||
_output.WriteLine($"Positive Y-axis: PineAtan2({y}, {x}) = {pine:F15}");
|
||||
_output.WriteLine($"Expected: π/2 = {expected:F15}");
|
||||
|
||||
Assert.Equal(expected, dotNet, precision: 14);
|
||||
Assert.Equal(expected, pine, precision: 14);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_AxisAligned_NegativeY()
|
||||
{
|
||||
// On negative Y-axis: x = 0, y < 0 → -π/2
|
||||
double y = -1.0, x = 0.0;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
double expected = -Math.PI / 2.0;
|
||||
|
||||
_output.WriteLine($"Negative Y-axis: Math.Atan2({y}, {x}) = {dotNet:F15}");
|
||||
_output.WriteLine($"Negative Y-axis: PineAtan2({y}, {x}) = {pine:F15}");
|
||||
_output.WriteLine($"Expected: -π/2 = {expected:F15}");
|
||||
|
||||
Assert.Equal(expected, dotNet, precision: 14);
|
||||
Assert.Equal(expected, pine, precision: 14);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_AxisAligned_PositiveX()
|
||||
{
|
||||
// On positive X-axis: x > 0, y = 0 → 0
|
||||
double y = 0.0, x = 1.0;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
double expected = 0.0;
|
||||
|
||||
_output.WriteLine($"Positive X-axis: Math.Atan2({y}, {x}) = {dotNet:F15}");
|
||||
_output.WriteLine($"Positive X-axis: PineAtan2({y}, {x}) = {pine:F15}");
|
||||
_output.WriteLine($"Expected: 0 = {expected:F15}");
|
||||
|
||||
Assert.Equal(expected, dotNet, precision: 14);
|
||||
Assert.Equal(expected, pine, precision: 14);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_AxisAligned_NegativeX()
|
||||
{
|
||||
// On negative X-axis: x < 0, y = 0 → π
|
||||
double y = 0.0, x = -1.0;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
double expected = Math.PI;
|
||||
|
||||
_output.WriteLine($"Negative X-axis: Math.Atan2({y}, {x}) = {dotNet:F15}");
|
||||
_output.WriteLine($"Negative X-axis: PineAtan2({y}, {x}) = {pine:F15}");
|
||||
_output.WriteLine($"Expected: π = {expected:F15}");
|
||||
|
||||
Assert.Equal(expected, dotNet, precision: 14);
|
||||
Assert.Equal(expected, pine, precision: 14);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_Origin_DotNetReturnsZero_PineThrows()
|
||||
{
|
||||
// Origin: x = 0, y = 0
|
||||
// Math.Atan2 returns 0 (by convention)
|
||||
// PineScript throws an error
|
||||
double y = 0.0, x = 0.0;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
|
||||
_output.WriteLine($"Origin: Math.Atan2({y}, {x}) = {dotNet:F15}");
|
||||
_output.WriteLine("Origin: PineScript throws ArgumentException");
|
||||
|
||||
Assert.Equal(0.0, dotNet);
|
||||
Assert.Throws<ArgumentException>(() => PineScriptAtan2(y, x));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_VerySmallValues_NumericalStability()
|
||||
{
|
||||
// Test numerical stability with very small values
|
||||
double y = 1e-300, x = 1e-300;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
double expected = Math.PI / 4.0; // 45 degrees
|
||||
|
||||
_output.WriteLine($"Small values: Math.Atan2({y:E}, {x:E}) = {dotNet:F15}");
|
||||
_output.WriteLine($"Small values: PineAtan2({y:E}, {x:E}) = {pine:F15}");
|
||||
_output.WriteLine($"Expected: π/4 = {expected:F15}");
|
||||
_output.WriteLine($"Difference = {Math.Abs(dotNet - pine):E}");
|
||||
|
||||
// Both should handle small values well
|
||||
Assert.Equal(expected, dotNet, precision: 10);
|
||||
Assert.Equal(expected, pine, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_VeryLargeValues_NumericalStability()
|
||||
{
|
||||
// Test numerical stability with very large values
|
||||
double y = 1e300, x = 1e300;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
double expected = Math.PI / 4.0; // 45 degrees
|
||||
|
||||
_output.WriteLine($"Large values: Math.Atan2({y:E}, {x:E}) = {dotNet:F15}");
|
||||
_output.WriteLine($"Large values: PineAtan2({y:E}, {x:E}) = {pine:F15}");
|
||||
_output.WriteLine($"Expected: π/4 = {expected:F15}");
|
||||
_output.WriteLine($"Difference = {Math.Abs(dotNet - pine):E}");
|
||||
|
||||
Assert.Equal(expected, dotNet, precision: 10);
|
||||
Assert.Equal(expected, pine, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_AspectRatioExtreme_TallVector()
|
||||
{
|
||||
// PineScript algorithm switches behavior when |y| > |x|
|
||||
// Test with y >> x
|
||||
double y = 1000.0, x = 1.0;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
|
||||
_output.WriteLine($"Tall vector: Math.Atan2({y}, {x}) = {dotNet:F15}");
|
||||
_output.WriteLine($"Tall vector: PineAtan2({y}, {x}) = {pine:F15}");
|
||||
_output.WriteLine($"Difference = {Math.Abs(dotNet - pine):E}");
|
||||
|
||||
// Should be very close to π/2
|
||||
Assert.True(Math.Abs(dotNet - pine) < 1e-12, $"Difference {Math.Abs(dotNet - pine):E} exceeds tolerance");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_AspectRatioExtreme_WideVector()
|
||||
{
|
||||
// Test with x >> y
|
||||
double y = 1.0, x = 1000.0;
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
|
||||
_output.WriteLine($"Wide vector: Math.Atan2({y}, {x}) = {dotNet:F15}");
|
||||
_output.WriteLine($"Wide vector: PineAtan2({y}, {x}) = {pine:F15}");
|
||||
_output.WriteLine($"Difference = {Math.Abs(dotNet - pine):E}");
|
||||
|
||||
// Should be very close to 0
|
||||
Assert.True(Math.Abs(dotNet - pine) < 1e-12, $"Difference {Math.Abs(dotNet - pine):E} exceeds tolerance");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0.5, 0.866025403784439)] // 30 degrees
|
||||
[InlineData(0.707106781186548, 0.707106781186548)] // 45 degrees
|
||||
[InlineData(0.866025403784439, 0.5)] // 60 degrees
|
||||
public void Atan2_CommonAngles_Match(double y, double x)
|
||||
{
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
|
||||
_output.WriteLine($"Math.Atan2({y}, {x}) = {dotNet:F15} rad = {dotNet * 180 / Math.PI:F10}°");
|
||||
_output.WriteLine($"PineAtan2({y}, {x}) = {pine:F15} rad = {pine * 180 / Math.PI:F10}°");
|
||||
_output.WriteLine($"Difference = {Math.Abs(dotNet - pine):E}");
|
||||
|
||||
Assert.Equal(dotNet, pine, precision: 13);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Atan2_FullCircle_360DegreesSweep()
|
||||
{
|
||||
// Sweep through 360 degrees and verify both implementations match
|
||||
const int steps = 360;
|
||||
double maxDiff = 0;
|
||||
int maxDiffStep = 0;
|
||||
|
||||
for (int i = 0; i < steps; i++)
|
||||
{
|
||||
double angle = i * 2.0 * Math.PI / steps;
|
||||
double y = Math.Sin(angle);
|
||||
double x = Math.Cos(angle);
|
||||
|
||||
// Skip origin (angle = 0 with x=1, y=0 is fine, but need to handle numerical zeros)
|
||||
if (Math.Abs(x) < 1e-15 && Math.Abs(y) < 1e-15)
|
||||
continue;
|
||||
|
||||
double dotNet = Math.Atan2(y, x);
|
||||
double pine = PineScriptAtan2(y, x);
|
||||
double diff = Math.Abs(dotNet - pine);
|
||||
|
||||
if (diff > maxDiff)
|
||||
{
|
||||
maxDiff = diff;
|
||||
maxDiffStep = i;
|
||||
}
|
||||
}
|
||||
|
||||
_output.WriteLine($"Full circle sweep: {steps} steps");
|
||||
_output.WriteLine($"Maximum difference: {maxDiff:E} at step {maxDiffStep} ({maxDiffStep}°)");
|
||||
|
||||
// Expect very small differences
|
||||
Assert.True(maxDiff < 1e-14, $"Maximum difference {maxDiff:E} exceeds tolerance");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Summary_ImplementationDifferences()
|
||||
{
|
||||
_output.WriteLine("=== Math.Atan2 vs PineScript atan2 Summary ===");
|
||||
_output.WriteLine("");
|
||||
_output.WriteLine("1. Origin handling:");
|
||||
_output.WriteLine(" - Math.Atan2(0, 0) returns 0");
|
||||
_output.WriteLine(" - PineScript throws an error");
|
||||
_output.WriteLine("");
|
||||
_output.WriteLine("2. Algorithm:");
|
||||
_output.WriteLine(" - Math.Atan2: IEEE standard implementation");
|
||||
_output.WriteLine(" - PineScript: Uses |y|/|x| or |x|/|y| based on which is larger");
|
||||
_output.WriteLine(" This avoids division by very small numbers");
|
||||
_output.WriteLine("");
|
||||
_output.WriteLine("3. Numerical stability:");
|
||||
_output.WriteLine(" - Both handle extreme values well");
|
||||
_output.WriteLine(" - PineScript's approach may be slightly more stable for extreme aspect ratios");
|
||||
_output.WriteLine("");
|
||||
_output.WriteLine("4. Recommendation:");
|
||||
_output.WriteLine(" - Use Math.Atan2 for general purposes (standard, well-tested)");
|
||||
_output.WriteLine(" - PineScript algorithm adds ~zero benefit in .NET");
|
||||
_output.WriteLine(" - Only difference is origin handling (error vs 0)");
|
||||
|
||||
Assert.True(true); // This is a documentation test
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ public sealed class Sum : AbstractBase
|
||||
private readonly TValuePublishedHandler _handler;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private struct State
|
||||
private record struct State
|
||||
{
|
||||
public double Sum; // Accumulated sum
|
||||
public double C; // First-order compensation
|
||||
|
||||
@@ -43,7 +43,7 @@ public sealed class Hamma : AbstractBase
|
||||
private bool _isNew = true;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private struct State
|
||||
private record struct State
|
||||
{
|
||||
public double LastValidValue;
|
||||
public bool IsInitialized;
|
||||
|
||||
@@ -29,7 +29,7 @@ public sealed class Frama : ITValuePublisher
|
||||
private readonly TValuePublishedHandler _handler;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct State
|
||||
private record struct State
|
||||
{
|
||||
public double Frama;
|
||||
public double LastHigh;
|
||||
|
||||
@@ -30,7 +30,7 @@ public sealed class Hema : AbstractBase
|
||||
private const double Ln2 = 0.693147180559945309417232121458176568;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct State
|
||||
private record struct State
|
||||
{
|
||||
public double EmaSlowRaw;
|
||||
public double EmaFastRaw;
|
||||
|
||||
@@ -210,9 +210,12 @@ public sealed class Mama : AbstractBase
|
||||
double alpha = _scaledFastLimit / delta;
|
||||
alpha = Math.Clamp(alpha, _slowLimit, _fastLimit);
|
||||
|
||||
// Final indicators
|
||||
_state.Mama = alpha * _priceBuffer[^1] + (1.0 - alpha) * _p_state.Mama;
|
||||
_state.Fama = FamaAlphaFactor * alpha * _state.Mama + (1.0 - FamaAlphaFactor * alpha) * _p_state.Fama;
|
||||
// Final indicators (using FMA for precision)
|
||||
double decay = 1.0 - alpha;
|
||||
_state.Mama = Math.FusedMultiplyAdd(_p_state.Mama, decay, alpha * _priceBuffer[^1]);
|
||||
double famaAlpha = FamaAlphaFactor * alpha;
|
||||
double famaDecay = 1.0 - famaAlpha;
|
||||
_state.Fama = Math.FusedMultiplyAdd(_p_state.Fama, famaDecay, famaAlpha * _state.Mama);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -442,9 +445,12 @@ public sealed class Mama : AbstractBase
|
||||
double alpha = scaledFastLimit / delta;
|
||||
alpha = Math.Clamp(alpha, slowLimit, fastLimit);
|
||||
|
||||
// Final indicators
|
||||
mama = alpha * priceBuffer[bufferIdx] + (1.0 - alpha) * p_mama;
|
||||
fama = FamaAlphaFactor * alpha * mama + (1.0 - FamaAlphaFactor * alpha) * p_fama;
|
||||
// Final indicators (using FMA for precision)
|
||||
double decay = 1.0 - alpha;
|
||||
mama = Math.FusedMultiplyAdd(p_mama, decay, alpha * priceBuffer[bufferIdx]);
|
||||
double famaAlpha = FamaAlphaFactor * alpha;
|
||||
double famaDecay = 1.0 - famaAlpha;
|
||||
fama = Math.FusedMultiplyAdd(p_fama, famaDecay, famaAlpha * mama);
|
||||
|
||||
// Update previous state
|
||||
p_i2 = i2;
|
||||
|
||||
Reference in New Issue
Block a user