Refactor and enhance various channel indicators for improved performance and stability

- Updated Codacy instructions to streamline usage guidelines.
- Refactored Bbands class to utilize ArrayPool for memory management, preventing stack overflow on large series.
- Changed Fcb class to use long for monotonic deques to avoid truncation issues.
- Enhanced Kchannel class to ensure safe defaults for non-finite values.
- Improved Maenv class to prevent double-priming during calculations.
- Modified Mmchannel class to ensure non-negative buffer indices and removed unnecessary state tracking.
- Updated Pchannel class to correctly reference IsHot state.
- Refined Regchannel class to avoid double-processing during calculations.
- Enhanced Starchannel class to sanitize non-finite values during calculations.
- Adjusted Stbands.Quantower.cs to allow finer control over multiplier precision.
- Updated Ubands class to only update last valid values on new bars.
- Modified Uchannel.Quantower.cs to allow for finer multiplier precision.
- Enhanced Vwapbands classes to include standard deviation calculations and ensure consistent array lengths.
- Refactored Vwapsd classes to include standard deviation outputs and ensure consistent array lengths.
- Updated MonotonicDeque to use long for indices to prevent overflow.
- Improved Mdape class to handle zero actual values with a substitute value for error calculation.
- Enhanced Rae class to ensure correct state management during updates.
- Refined Wmape class to simplify the logic for finding last valid actual and predicted values.
- Updated Cmf.Quantower classes to ensure MinHistoryDepths reflects the current period.
This commit is contained in:
Miha Kralj
2026-01-27 23:48:33 -08:00
parent 8ac15f1efa
commit a9e72dae0d
29 changed files with 464 additions and 114 deletions
+61 -21
View File
@@ -1,3 +1,4 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -168,15 +169,30 @@ public sealed class Bbands : AbstractBase
int len = sourceSpan.Length;
TSeries middleSeries = new(capacity: len);
Span<double> middleSpan = stackalloc double[len];
Span<double> upperSpan = stackalloc double[len];
Span<double> lowerSpan = stackalloc double[len];
Calculate(sourceSpan, middleSpan, upperSpan, lowerSpan, _period, _multiplier);
// Use ArrayPool to avoid stack overflow for large series
double[] middleRented = ArrayPool<double>.Shared.Rent(len);
double[] upperRented = ArrayPool<double>.Shared.Rent(len);
double[] lowerRented = ArrayPool<double>.Shared.Rent(len);
for (int i = 0; i < len; i++)
try
{
middleSeries.Add(timeSpan[i], middleSpan[i], isNew: true);
Span<double> middleSpan = middleRented.AsSpan(0, len);
Span<double> upperSpan = upperRented.AsSpan(0, len);
Span<double> lowerSpan = lowerRented.AsSpan(0, len);
Calculate(sourceSpan, middleSpan, upperSpan, lowerSpan, _period, _multiplier);
for (int i = 0; i < len; i++)
{
middleSeries.Add(timeSpan[i], middleSpan[i], isNew: true);
}
}
finally
{
ArrayPool<double>.Shared.Return(middleRented);
ArrayPool<double>.Shared.Return(upperRented);
ArrayPool<double>.Shared.Return(lowerRented);
}
// Restore state from the last period values
@@ -257,8 +273,10 @@ public sealed class Bbands : AbstractBase
// Calculate standard deviation and bands using O(n) rolling sums
// Instead of O(n²) nested loop, maintain running sum and sumSq
// Track count of finite values to properly compute mean/variance
double rollingSum = 0.0;
double rollingSumSq = 0.0;
int finiteCount = 0;
// Initialize rolling sums for first window
for (int i = 0; i < Math.Min(period, len); i++)
@@ -268,6 +286,7 @@ public sealed class Bbands : AbstractBase
{
rollingSum += val;
rollingSumSq += val * val;
finiteCount++;
}
if (i < period - 1)
@@ -280,13 +299,22 @@ public sealed class Bbands : AbstractBase
// 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;
if (finiteCount == period)
{
double mean = rollingSum / finiteCount;
double variance = (rollingSumSq / finiteCount) - (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;
}
else
{
// Not all values in window are finite, emit NaN
upper[period - 1] = double.NaN;
lower[period - 1] = double.NaN;
}
}
// Process remaining bars with O(1) rolling update
@@ -298,6 +326,7 @@ public sealed class Bbands : AbstractBase
{
rollingSum -= outgoing;
rollingSumSq -= outgoing * outgoing;
finiteCount--;
}
// Add incoming value (current)
@@ -306,18 +335,29 @@ public sealed class Bbands : AbstractBase
{
rollingSum += incoming;
rollingSumSq += incoming * incoming;
finiteCount++;
}
// 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
// Only compute bands when all values in window are finite
if (finiteCount == period)
{
// Calculate variance from rolling sums: Var = E[X²] - E[X]²
double mean = rollingSum / finiteCount;
double variance = (rollingSumSq / finiteCount) - (mean * mean);
variance = Math.Max(0.0, variance); // Guard against negative due to floating point
double stdDev = Math.Sqrt(variance);
double offset = multiplier * stdDev;
double stdDev = Math.Sqrt(variance);
double offset = multiplier * stdDev;
upper[i] = middle[i] + offset;
lower[i] = middle[i] - offset;
upper[i] = middle[i] + offset;
lower[i] = middle[i] - offset;
}
else
{
// Window contains non-finite values, emit NaN
upper[i] = double.NaN;
lower[i] = double.NaN;
}
}
}
}
+15 -12
View File
@@ -20,9 +20,9 @@ public sealed class Fcb : ITValuePublisher
private readonly double[] _hBuf;
private readonly double[] _lBuf;
// Monotonic deques (store indices)
private readonly int[] _hDeque;
private readonly int[] _lDeque;
// Monotonic deques (store indices as long to avoid truncation)
private readonly long[] _hDeque;
private readonly long[] _lDeque;
// Deque state
private int _hHead;
@@ -68,8 +68,8 @@ public sealed class Fcb : ITValuePublisher
_hBuf = new double[_period];
_lBuf = new double[_period];
_hDeque = new int[_period];
_lDeque = new int[_period];
_hDeque = new long[_period];
_lDeque = new long[_period];
Name = $"Fcb({period})";
_barHandler = HandleBar;
@@ -128,7 +128,7 @@ public sealed class Fcb : ITValuePublisher
while (_hCount > 0)
{
int backIdx = (_hHead + _hCount - 1) % _period;
int bufIdx = _hDeque[backIdx] % _period;
int bufIdx = (int)(_hDeque[backIdx] % _period);
if (_hBuf[bufIdx] <= value)
{
_hCount--;
@@ -140,7 +140,7 @@ public sealed class Fcb : ITValuePublisher
}
int tail = (_hHead + _hCount) % _period;
_hDeque[tail] = (int)logicalIndex;
_hDeque[tail] = logicalIndex;
_hCount++;
}
@@ -157,7 +157,7 @@ public sealed class Fcb : ITValuePublisher
while (_lCount > 0)
{
int backIdx = (_lHead + _lCount - 1) % _period;
int bufIdx = _lDeque[backIdx] % _period;
int bufIdx = (int)(_lDeque[backIdx] % _period);
if (_lBuf[bufIdx] >= value)
{
_lCount--;
@@ -169,7 +169,7 @@ public sealed class Fcb : ITValuePublisher
}
int tail = (_lHead + _lCount) % _period;
_lDeque[tail] = (int)logicalIndex;
_lDeque[tail] = logicalIndex;
_lCount++;
}
@@ -289,8 +289,8 @@ public sealed class Fcb : ITValuePublisher
RebuildDeques();
}
double top = _hBuf[_hDeque[_hHead] % _period];
double bot = _lBuf[_lDeque[_lHead] % _period];
double top = _hBuf[(int)(_hDeque[_hHead] % _period)];
double bot = _lBuf[(int)(_lDeque[_lHead] % _period)];
double mid = (top + bot) * 0.5;
if (!_state.IsHot && _index + 1 >= WarmupPeriod)
@@ -537,7 +537,10 @@ public sealed class Fcb : ITValuePublisher
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Fcb Indicator) Calculate(TBarSeries source, int period = 20)
{
var indicator = new Fcb(source, period);
// Use parameterless constructor to avoid double-priming:
// The Fcb(source, period) constructor already calls Prime(source),
// so calling Update(source) afterwards would Prime again.
var indicator = new Fcb(period);
var results = indicator.Update(source);
return (results, indicator);
}
+18 -3
View File
@@ -103,28 +103,43 @@ public sealed class Kchannel : ITValuePublisher
{
_state = _state with { LastValidClose = close };
}
else
else if (double.IsFinite(_state.LastValidClose))
{
close = _state.LastValidClose;
}
else
{
// Neither input nor stored value is finite - use safe default
close = 0.0;
}
if (double.IsFinite(high))
{
_state = _state with { LastValidHigh = high };
}
else
else if (double.IsFinite(_state.LastValidHigh))
{
high = _state.LastValidHigh;
}
else
{
// Neither input nor stored value is finite - use safe default
high = 0.0;
}
if (double.IsFinite(low))
{
_state = _state with { LastValidLow = low };
}
else
else if (double.IsFinite(_state.LastValidLow))
{
low = _state.LastValidLow;
}
else
{
// Neither input nor stored value is finite - use safe default
low = 0.0;
}
return (close, high, low);
}
+4 -1
View File
@@ -603,7 +603,10 @@ public sealed class Maenv : ITValuePublisher
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Maenv Indicator) Calculate(TSeries source, int period = 20, double percentage = 1.0, MaenvType maType = MaenvType.EMA)
{
var indicator = new Maenv(source, period, percentage, maType);
// Use parameterless constructor to avoid double-priming:
// The Maenv(source, ...) constructor already calls Prime(source),
// so calling Update(source) afterwards would Prime again.
var indicator = new Maenv(period, percentage, maType);
var results = indicator.Update(source);
return (results, indicator);
}
+10 -9
View File
@@ -23,7 +23,7 @@ public sealed class Mmchannel : ITValuePublisher
private long _index;
[StructLayout(LayoutKind.Auto)]
private record struct State(double LastValidHigh, double LastValidLow, bool IsHot);
private record struct State(double LastValidHigh, double LastValidLow);
private State _state;
private State _p_state;
@@ -52,7 +52,7 @@ public sealed class Mmchannel : ITValuePublisher
_minDeque = new MonotonicDeque(_period);
_count = 0;
_index = -1;
_state = new State(double.NaN, double.NaN, false);
_state = new State(double.NaN, double.NaN);
_p_state = _state;
Name = $"Mmchannel({period})";
@@ -112,7 +112,8 @@ public sealed class Mmchannel : ITValuePublisher
_state = _p_state;
}
int bufIdx = (int)(_index % _period);
// Defensive guard: ensure non-negative buffer index even if _index is -1 (shouldn't happen but safeguard)
int bufIdx = _index < 0 ? 0 : (int)(_index % _period);
var (high, low) = GetValid(input.High, input.Low);
// If still no valid data, return NaN placeholders
@@ -143,11 +144,6 @@ public sealed class Mmchannel : ITValuePublisher
double top = _maxDeque.GetExtremum(_hBuf);
double bot = _minDeque.GetExtremum(_lBuf);
if (!IsHot && _count >= _period)
{
_state = _state with { IsHot = true };
}
// Last returns Upper by default for single-value compatibility
Last = new TValue(input.Time, top);
Upper = new TValue(input.Time, top);
@@ -218,7 +214,7 @@ public sealed class Mmchannel : ITValuePublisher
_minDeque.Reset();
_count = 0;
_index = -1;
_state = new State(double.NaN, double.NaN, false);
_state = new State(double.NaN, double.NaN);
_p_state = _state;
Last = default;
Upper = default;
@@ -262,6 +258,11 @@ public sealed class Mmchannel : ITValuePublisher
public static (TSeries Upper, TSeries Lower) Batch(TBarSeries source, int period)
{
if (source == null || source.Count == 0)
{
return (new TSeries([], []), new TSeries([], []));
}
int len = source.Count;
var tUpper = new List<long>(len);
var vUpper = new List<double>(len);
+1 -1
View File
@@ -237,7 +237,7 @@ public sealed class Pchannel : ITValuePublisher
double bot = _lBuf[_lDeque[_lHead] % _period];
double mid = (top + bot) * 0.5;
if (!IsHot && _count >= _period)
if (!_state.IsHot && _count >= _period)
{
_state = _state with { IsHot = true };
}
+3 -1
View File
@@ -464,7 +464,9 @@ public sealed class Regchannel : ITValuePublisher
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Regchannel Indicator) Calculate(TSeries source, int period = 20, double multiplier = 2.0)
{
var indicator = new Regchannel(source, period, multiplier);
// Use parameterless constructor to avoid double-processing: new Regchannel(source, ...) calls Prime(source),
// then Update(source) would call Prime again.
var indicator = new Regchannel(period, multiplier);
var results = indicator.Update(source);
return (results, indicator);
}
+33
View File
@@ -322,12 +322,45 @@ public sealed class Starchannel : ITValuePublisher
upper[0] = close[0];
lower[0] = close[0];
// Track last valid values for sanitization
double lastValidClose = close[0];
double lastValidHigh = high[0];
double lastValidLow = low[0];
for (int i = 1; i < len; i++)
{
double c = close[i];
double h = high[i];
double l = low[i];
// Sanitize non-finite values (match Update/GetValid behavior)
if (double.IsFinite(c))
{
lastValidClose = c;
}
else
{
c = lastValidClose;
}
if (double.IsFinite(h))
{
lastValidHigh = h;
}
else
{
h = lastValidHigh;
}
if (double.IsFinite(l))
{
lastValidLow = l;
}
else
{
l = lastValidLow;
}
// SMA: add current, subtract oldest if beyond window
if (i < period)
{
+1 -1
View File
@@ -8,7 +8,7 @@ public class StbandsIndicator : Indicator, IWatchlistIndicator
[InputParameter("Period", sortIndex: 1, minimum: 1, maximum: 1000, increment: 1, decimalPlaces: 0)]
public int Period { get; set; } = 10;
[InputParameter("Multiplier", sortIndex: 2, minimum: 0.1, maximum: 10.0, increment: 0.1, decimalPlaces: 1)]
[InputParameter("Multiplier", sortIndex: 2, minimum: 0.001, maximum: 10.0, increment: 0.1, decimalPlaces: 3)]
public double Multiplier { get; set; } = 3.0;
[InputParameter("Show cold values", sortIndex: 21)]
+7 -3
View File
@@ -124,11 +124,15 @@ public sealed class Ubands : AbstractBase
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private double GetFiniteValue(double value)
private double GetFiniteValue(double value, bool isNew)
{
if (double.IsFinite(value))
{
_state.LastValidValue = value;
// Only update LastValidValue on new bars to avoid corrupting restored state during corrections
if (isNew)
{
_state = _state with { LastValidValue = value };
}
return value;
}
return double.IsFinite(_state.LastValidValue) ? _state.LastValidValue : 0;
@@ -149,7 +153,7 @@ public sealed class Ubands : AbstractBase
_state = _p_state;
}
double val = GetFiniteValue(input.Value);
double val = GetFiniteValue(input.Value, isNew);
// Initialize on first value
if (!_state.IsInitialized)
+1 -1
View File
@@ -11,7 +11,7 @@ public class UchannelIndicator : Indicator, IWatchlistIndicator
[InputParameter("Center Period", sortIndex: 2, minimum: 1, maximum: 1000, increment: 1, decimalPlaces: 0)]
public int CenterPeriod { get; set; } = 20;
[InputParameter("Multiplier", sortIndex: 3, minimum: 0.1, maximum: 10.0, increment: 0.1, decimalPlaces: 1)]
[InputParameter("Multiplier", sortIndex: 3, minimum: 0.001, maximum: 10.0, increment: 0.1, decimalPlaces: 3)]
public double Multiplier { get; set; } = 1.0;
[InputParameter("Show cold values", sortIndex: 21)]
+5 -3
View File
@@ -441,17 +441,18 @@ public class VwapbandsTests
double[] upper2 = new double[5];
double[] lower2 = new double[5];
double[] vwap = new double[5];
double[] stdDev = new double[5];
double[] wrongSize = new double[3];
// Multiplier must be >= MinMultiplier
Assert.Throws<ArgumentOutOfRangeException>(() =>
Vwapbands.Calculate(price.AsSpan(), volume.AsSpan(),
upper1.AsSpan(), lower1.AsSpan(), upper2.AsSpan(), lower2.AsSpan(), vwap.AsSpan(), 0));
upper1.AsSpan(), lower1.AsSpan(), upper2.AsSpan(), lower2.AsSpan(), vwap.AsSpan(), stdDev.AsSpan(), 0));
// All arrays must be same length
Assert.Throws<ArgumentException>(() =>
Vwapbands.Calculate(price.AsSpan(), volume.AsSpan(),
wrongSize.AsSpan(), lower1.AsSpan(), upper2.AsSpan(), lower2.AsSpan(), vwap.AsSpan(), 1.0));
wrongSize.AsSpan(), lower1.AsSpan(), upper2.AsSpan(), lower2.AsSpan(), vwap.AsSpan(), stdDev.AsSpan(), 1.0));
}
[Fact]
@@ -464,9 +465,10 @@ public class VwapbandsTests
double[] upper2 = new double[5];
double[] lower2 = new double[5];
double[] vwap = new double[5];
double[] stdDev = new double[5];
Vwapbands.Calculate(price.AsSpan(), volume.AsSpan(),
upper1.AsSpan(), lower1.AsSpan(), upper2.AsSpan(), lower2.AsSpan(), vwap.AsSpan(), 1.0);
upper1.AsSpan(), lower1.AsSpan(), upper2.AsSpan(), lower2.AsSpan(), vwap.AsSpan(), stdDev.AsSpan(), 1.0);
foreach (var val in vwap)
{
@@ -124,11 +124,12 @@ public sealed class VwapbandsValidationTests : IDisposable
double[] spanLower1 = new double[bars.Count];
double[] spanUpper2 = new double[bars.Count];
double[] spanLower2 = new double[bars.Count];
double[] spanStdDev = new double[bars.Count];
Vwapbands.Calculate(price.AsSpan(), volume.AsSpan(),
spanUpper1.AsSpan(), spanLower1.AsSpan(),
spanUpper2.AsSpan(), spanLower2.AsSpan(),
spanVwap.AsSpan(), multiplier);
spanVwap.AsSpan(), spanStdDev.AsSpan(), multiplier);
// Compare last 100 values
int compareCount = Math.Min(100, bars.Count - 2);
+12 -1
View File
@@ -351,6 +351,15 @@ public sealed class Vwapbands : AbstractBase
/// <summary>
/// Calculates VWAP Bands using span arrays.
/// </summary>
/// <param name="price">Source price values (typically HLC3)</param>
/// <param name="volume">Volume values</param>
/// <param name="upper1">Output span for upper band at 1σ</param>
/// <param name="lower1">Output span for lower band at 1σ</param>
/// <param name="upper2">Output span for upper band at 2σ</param>
/// <param name="lower2">Output span for lower band at 2σ</param>
/// <param name="vwap">Output span for VWAP values</param>
/// <param name="stdDev">Output span for standard deviation values</param>
/// <param name="multiplier">Band multiplier (default 1.0)</param>
public static void Calculate(
ReadOnlySpan<double> price,
ReadOnlySpan<double> volume,
@@ -359,11 +368,12 @@ public sealed class Vwapbands : AbstractBase
Span<double> upper2,
Span<double> lower2,
Span<double> vwap,
Span<double> stdDev,
double multiplier = DefaultMultiplier)
{
int len = price.Length;
if (len != volume.Length || len != upper1.Length || len != lower1.Length ||
len != upper2.Length || len != lower2.Length || len != vwap.Length)
len != upper2.Length || len != lower2.Length || len != vwap.Length || len != stdDev.Length)
{
throw new ArgumentException("All spans must have the same length.", nameof(price));
}
@@ -408,6 +418,7 @@ public sealed class Vwapbands : AbstractBase
double stdev = Math.Sqrt(variance);
vwap[i] = vwapVal;
stdDev[i] = stdev;
upper1[i] = vwapVal + multiplier * stdev;
lower1[i] = vwapVal - multiplier * stdev;
upper2[i] = vwapVal + 2.0 * multiplier * stdev;
+22 -2
View File
@@ -31,8 +31,8 @@ public class VwapsdIndicator : Indicator, IWatchlistIndicator
Description = "Volume weighted average price with configurable standard deviation bands";
VwapSeries = new("VWAP", Color.Blue, 2, LineStyle.Solid);
UpperSeries = new($"Upper (+{NumDevs}σ)", Color.Red, 1, LineStyle.Solid);
LowerSeries = new($"Lower (-{NumDevs}σ)", Color.Green, 1, LineStyle.Solid);
UpperSeries = new("Upper", Color.Red, 1, LineStyle.Solid);
LowerSeries = new("Lower", Color.Green, 1, LineStyle.Solid);
WidthSeries = new("Width", Color.Gray, 1, LineStyle.Dot);
AddLineSeries(VwapSeries);
@@ -47,9 +47,29 @@ public class VwapsdIndicator : Indicator, IWatchlistIndicator
protected override void OnInit()
{
vwapsd = new(NumDevs);
if (UpperSeries != null)
{
UpperSeries.Name = $"Upper (+{NumDevs:F1}σ)";
}
if (LowerSeries != null)
{
LowerSeries.Name = $"Lower (-{NumDevs:F1}σ)";
}
base.OnInit();
}
private void UpdateSeriesNames()
{
if (UpperSeries != null)
{
UpperSeries.Name = $"Upper (+{NumDevs:F1}σ)";
}
if (LowerSeries != null)
{
LowerSeries.Name = $"Lower (-{NumDevs:F1}σ)";
}
}
protected override void OnUpdate(UpdateArgs args)
{
var item = HistoricalData[0, SeekOriginHistory.End];
+6 -4
View File
@@ -467,22 +467,23 @@ public class VwapsdTests
double[] upper = new double[5];
double[] lower = new double[5];
double[] vwap = new double[5];
double[] stdDev = new double[5];
double[] wrongSize = new double[3];
// NumDevs must be >= MinNumDevs
Assert.Throws<ArgumentOutOfRangeException>(() =>
Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(),
upper.AsSpan(), lower.AsSpan(), vwap.AsSpan(), 0));
upper.AsSpan(), lower.AsSpan(), vwap.AsSpan(), stdDev.AsSpan(), 0));
// NumDevs must be <= MaxNumDevs
Assert.Throws<ArgumentOutOfRangeException>(() =>
Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(),
upper.AsSpan(), lower.AsSpan(), vwap.AsSpan(), 6.0));
upper.AsSpan(), lower.AsSpan(), vwap.AsSpan(), stdDev.AsSpan(), 6.0));
// All arrays must be same length
Assert.Throws<ArgumentException>(() =>
Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(),
wrongSize.AsSpan(), lower.AsSpan(), vwap.AsSpan(), 1.0));
wrongSize.AsSpan(), lower.AsSpan(), vwap.AsSpan(), stdDev.AsSpan(), 1.0));
}
[Fact]
@@ -493,9 +494,10 @@ public class VwapsdTests
double[] upper = new double[5];
double[] lower = new double[5];
double[] vwap = new double[5];
double[] stdDev = new double[5];
Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(),
upper.AsSpan(), lower.AsSpan(), vwap.AsSpan(), 1.0);
upper.AsSpan(), lower.AsSpan(), vwap.AsSpan(), stdDev.AsSpan(), 1.0);
foreach (var val in vwap)
{
@@ -118,10 +118,11 @@ public sealed class VwapsdValidationTests : IDisposable
double[] spanVwap = new double[bars.Count];
double[] spanUpper = new double[bars.Count];
double[] spanLower = new double[bars.Count];
double[] spanStdDev = new double[bars.Count];
Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(),
spanUpper.AsSpan(), spanLower.AsSpan(),
spanVwap.AsSpan(), numDevs);
spanVwap.AsSpan(), spanStdDev.AsSpan(), numDevs);
// Compare last 100 values
int compareCount = Math.Min(100, bars.Count - 2);
+10 -1
View File
@@ -337,16 +337,24 @@ public sealed class Vwapsd : AbstractBase
/// <summary>
/// Calculates VWAP SD Bands using span arrays.
/// </summary>
/// <param name="price">Source price values (typically HLC3)</param>
/// <param name="volume">Volume values</param>
/// <param name="upper">Output span for upper band</param>
/// <param name="lower">Output span for lower band</param>
/// <param name="vwap">Output span for VWAP values</param>
/// <param name="stdDev">Output span for standard deviation values</param>
/// <param name="numDevs">Number of standard deviations for bands (default 2.0)</param>
public static void Calculate(
ReadOnlySpan<double> price,
ReadOnlySpan<double> volume,
Span<double> upper,
Span<double> lower,
Span<double> vwap,
Span<double> stdDev,
double numDevs = DefaultNumDevs)
{
int len = price.Length;
if (len != volume.Length || len != upper.Length || len != lower.Length || len != vwap.Length)
if (len != volume.Length || len != upper.Length || len != lower.Length || len != vwap.Length || len != stdDev.Length)
{
throw new ArgumentException("All spans must have the same length.", nameof(price));
}
@@ -396,6 +404,7 @@ public sealed class Vwapsd : AbstractBase
double stdev = Math.Sqrt(variance);
vwap[i] = vwapVal;
stdDev[i] = stdev;
upper[i] = vwapVal + numDevs * stdev;
lower[i] = vwapVal - numDevs * stdev;
}
+8 -8
View File
@@ -16,7 +16,7 @@ namespace QuanTAlib;
[SkipLocalsInit]
public sealed class MonotonicDeque
{
private readonly int[] _deque;
private readonly long[] _deque;
private readonly int _period;
private int _head;
private int _count;
@@ -24,7 +24,7 @@ public sealed class MonotonicDeque
/// <summary>
/// Gets the current front index (the index of the current extremum).
/// </summary>
public int FrontIndex => _count > 0 ? _deque[_head] : -1;
public long FrontIndex => _count > 0 ? _deque[_head] : -1;
/// <summary>
/// Gets the current element count in the deque.
@@ -43,7 +43,7 @@ public sealed class MonotonicDeque
}
_period = period;
_deque = new int[period];
_deque = new long[period];
_head = 0;
_count = 0;
}
@@ -70,7 +70,7 @@ public sealed class MonotonicDeque
while (_count > 0)
{
int backIdx = (_head + _count - 1) % _period;
int bufIdx = _deque[backIdx] % _period;
int bufIdx = (int)(_deque[backIdx] % _period);
if (buffer[bufIdx] <= value)
{
_count--;
@@ -83,7 +83,7 @@ public sealed class MonotonicDeque
// Push new index
int tail = (_head + _count) % _period;
_deque[tail] = (int)logicalIndex;
_deque[tail] = logicalIndex;
_count++;
}
@@ -109,7 +109,7 @@ public sealed class MonotonicDeque
while (_count > 0)
{
int backIdx = (_head + _count - 1) % _period;
int bufIdx = _deque[backIdx] % _period;
int bufIdx = (int)(_deque[backIdx] % _period);
if (buffer[bufIdx] >= value)
{
_count--;
@@ -122,7 +122,7 @@ public sealed class MonotonicDeque
// Push new index
int tail = (_head + _count) % _period;
_deque[tail] = (int)logicalIndex;
_deque[tail] = logicalIndex;
_count++;
}
@@ -134,7 +134,7 @@ public sealed class MonotonicDeque
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double GetExtremum(double[] buffer)
{
return _count > 0 ? buffer[_deque[_head] % _period] : double.NaN;
return _count > 0 ? buffer[(int)(_deque[_head] % _period)] : double.NaN;
}
/// <summary>
+9 -8
View File
@@ -309,17 +309,18 @@ public class MdapeTests
}
[Fact]
public void Calculate_ZeroActual_ReturnsZeroError()
public void Calculate_ZeroActual_UsesSubstituteValue()
{
// When actual is zero or near-zero, should return 0 error (epsilon protection)
// When actual is zero or near-zero, implementation substitutes 1.0 fallback
// to avoid division by zero (epsilon protection means substitute, not return 0)
var mdape = new Mdape(3);
mdape.Update(0.0, 10);
mdape.Update(0.0, 20);
mdape.Update(0.0, 30);
mdape.Update(0.0, 10); // actual=1.0 (substituted), pred=10 → |1-10|/1 * 100 = 900%
mdape.Update(0.0, 20); // actual=1.0 (substituted), pred=20 → |1-20|/1 * 100 = 1900%
mdape.Update(0.0, 30); // actual=1.0 (substituted), pred=30 → |1-30|/1 * 100 = 2900%
// With epsilon protection, all errors are 0
Assert.Equal(0.0, mdape.Last.Value, Precision);
// Median of [900, 1900, 2900] = 1900
Assert.Equal(1900.0, mdape.Last.Value, Precision);
}
[Fact]
@@ -361,4 +362,4 @@ public class MdapeTests
Assert.Equal(mdape1.Last.Value, mdape2.Last.Value, Precision);
}
}
}
+7 -4
View File
@@ -74,9 +74,12 @@ public sealed class Mdape : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private TValue UpdateCore(DateTime time, double actualVal, double predictedVal, bool isNew)
{
if (!double.IsFinite(actualVal))
// Validate actual: must be finite AND have sufficient magnitude (matches Batch logic)
if (!double.IsFinite(actualVal) || Math.Abs(actualVal) < 1e-10)
{
actualVal = double.IsFinite(_state.LastValidActual) ? _state.LastValidActual : 1.0;
actualVal = double.IsFinite(_state.LastValidActual) && Math.Abs(_state.LastValidActual) >= 1e-10
? _state.LastValidActual
: 1.0;
}
else
{
@@ -92,10 +95,10 @@ public sealed class Mdape : AbstractBase
_state.LastValidPredicted = predictedVal;
}
// Calculate absolute percentage error
// Calculate absolute percentage error (absActual guaranteed >= 1e-10 by validation above)
double absActual = Math.Abs(actualVal);
double absError = Math.Abs(actualVal - predictedVal);
double percentageError = absActual > 1e-10 ? (absError / absActual) * 100.0 : 0.0;
double percentageError = (absError / absActual) * 100.0;
if (isNew)
{
+12 -5
View File
@@ -62,6 +62,17 @@ public sealed class Rae : AbstractBase
double actualVal = actual.Value;
double predictedVal = predicted.Value;
// Snapshot BEFORE any mutations for correct rollback
if (isNew)
{
_p_state = _state;
}
else
{
_state = _p_state;
}
// Sanitize non-finite values AFTER snapshot/restore
if (!double.IsFinite(actualVal))
{
actualVal = double.IsFinite(_state.LastValidActual) ? _state.LastValidActual : 0.0;
@@ -82,8 +93,6 @@ public sealed class Rae : AbstractBase
if (isNew)
{
_p_state = _state;
// Update actual buffer for mean calculation
double removedActual = _actualBuffer.Count == _actualBuffer.Capacity ? _actualBuffer.Oldest : 0.0;
_state.ActualSum = _state.ActualSum - removedActual + actualVal;
@@ -115,8 +124,6 @@ public sealed class Rae : AbstractBase
}
else
{
_state = _p_state;
// Update buffers and recalculate sums (buffer state is inconsistent with _p_state)
_actualBuffer.UpdateNewest(actualVal);
_state.ActualSum = _actualBuffer.RecalculateSum();
@@ -348,4 +355,4 @@ public sealed class Rae : AbstractBase
}
}
}
}
}
+12 -12
View File
@@ -222,22 +222,22 @@ public sealed class Wmape : AbstractBase
double lastValidActual = 0;
double lastValidPredicted = 0;
for (int k = 0; k < len; k++)
{
if (double.IsFinite(actual[k]))
for (int k = 0; k < len; k++)
{
lastValidActual = actual[k];
break;
if (double.IsFinite(actual[k]))
{
lastValidActual = actual[k];
break;
}
}
}
for (int k = 0; k < len; k++)
{
if (double.IsFinite(predicted[k]))
for (int k = 0; k < len; k++)
{
lastValidPredicted = predicted[k];
break;
if (double.IsFinite(predicted[k]))
{
lastValidPredicted = predicted[k];
break;
}
}
}
int bufferIndex = 0;
int i = 0;
+2 -2
View File
@@ -13,7 +13,7 @@ public class CmfIndicatorTests
Assert.Equal(20, indicator.Period);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
Assert.Equal(20, CmfIndicator.MinHistoryDepths);
Assert.Equal(20, indicator.MinHistoryDepths);
}
[Fact]
@@ -28,7 +28,7 @@ public class CmfIndicatorTests
{
var indicator = new CmfIndicator();
Assert.Equal(20, CmfIndicator.MinHistoryDepths);
Assert.Equal(20, indicator.MinHistoryDepths);
Assert.Equal(20, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
+2 -2
View File
@@ -16,8 +16,8 @@ public sealed class CmfIndicator : Indicator, IWatchlistIndicator
private Cmf _cmf = null!;
private readonly LineSeries _series;
public static int MinHistoryDepths => 20;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public int MinHistoryDepths => Period;
int IWatchlistIndicator.MinHistoryDepths => Period;
public override string ShortName => $"CMF({Period})";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/volume/cmf/Cmf.Quantower.cs";