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

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