mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-18 10:38:05 +00:00
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:
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user