volume indicators

This commit is contained in:
Miha Kralj
2026-01-30 12:47:25 -08:00
parent 76d2b50cbb
commit 7b3a6520d2
99 changed files with 9539 additions and 283 deletions
+31 -10
View File
@@ -333,22 +333,43 @@ public sealed class Apchannel : AbstractBase
{
int length = sourceHigh.Length;
// Initialize first values with NaN handling
double highEma = double.IsFinite(sourceHigh[0]) ? sourceHigh[0] : 0;
double lowEma = double.IsFinite(sourceLow[0]) ? sourceLow[0] : 0;
double lastValidHigh = highEma;
double lastValidLow = lowEma;
// Scan for first finite values in both high and low arrays
double lastValidHigh = 0;
double lastValidLow = 0;
int firstValidIdx = 0;
upperBand[0] = highEma;
lowerBand[0] = lowEma;
for (int i = 0; i < length; i++)
{
if (double.IsFinite(sourceHigh[i]) && double.IsFinite(sourceLow[i]))
{
lastValidHigh = sourceHigh[i];
lastValidLow = sourceLow[i];
firstValidIdx = i;
break;
}
}
// Early return for single-element arrays
if (length == 1)
// Fill NaN for indices before first valid
for (int i = 0; i < firstValidIdx; i++)
{
upperBand[i] = double.NaN;
lowerBand[i] = double.NaN;
}
// Initialize with first valid values
double highEma = lastValidHigh;
double lowEma = lastValidLow;
upperBand[firstValidIdx] = highEma;
lowerBand[firstValidIdx] = lowEma;
// Early return if no more elements after first valid
if (firstValidIdx >= length - 1)
{
return;
}
for (int i = 1; i < length; i++)
for (int i = firstValidIdx + 1; i < length; i++)
{
double high = sourceHigh[i];
double low = sourceLow[i];
+2 -1
View File
@@ -679,7 +679,8 @@ public sealed class Apz : ITValuePublisher
int len = close.Length;
for (int k = 0; k < len; k++)
{
if (double.IsFinite(close[k]))
// Check all three values are finite before assigning state
if (double.IsFinite(close[k]) && double.IsFinite(high[k]) && double.IsFinite(low[k]))
{
state.LastValidPrice = close[k];
state.LastValidHigh = high[k];
+1 -1
View File
@@ -145,7 +145,7 @@ public sealed class Dchannel : ITValuePublisher
double bot = _minDeque.GetExtremum(_lBuf);
double mid = (top + bot) * 0.5;
if (!IsHot && _count >= _period)
if (!_state.IsHot && _count >= _period)
{
_state = _state with { IsHot = true };
}
+8 -8
View File
@@ -400,8 +400,8 @@ public sealed class Fcb : ITValuePublisher
// Allocate buffers for fractal tracking and deques
double[] hBuf = ArrayPool<double>.Shared.Rent(period);
double[] lBuf = ArrayPool<double>.Shared.Rent(period);
int[] hDeque = ArrayPool<int>.Shared.Rent(period);
int[] lDeque = ArrayPool<int>.Shared.Rent(period);
long[] hDeque = ArrayPool<long>.Shared.Rent(period);
long[] lDeque = ArrayPool<long>.Shared.Rent(period);
try
{
@@ -452,7 +452,7 @@ public sealed class Fcb : ITValuePublisher
while (hCount > 0)
{
int backIdx = (hHead + hCount - 1) % period;
int bIdx = hDeque[backIdx] % period;
int bIdx = (int)(hDeque[backIdx] % period);
if (hBuf[bIdx] <= hiFractal)
{
hCount--;
@@ -475,7 +475,7 @@ public sealed class Fcb : ITValuePublisher
while (lCount > 0)
{
int backIdx = (lHead + lCount - 1) % period;
int bIdx = lDeque[backIdx] % period;
int bIdx = (int)(lDeque[backIdx] % period);
if (lBuf[bIdx] >= loFractal)
{
lCount--;
@@ -489,8 +489,8 @@ public sealed class Fcb : ITValuePublisher
lDeque[tail] = i;
lCount++;
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)];
upper[i] = top;
lower[i] = bot;
middle[i] = (top + bot) * 0.5;
@@ -500,8 +500,8 @@ public sealed class Fcb : ITValuePublisher
{
ArrayPool<double>.Shared.Return(hBuf);
ArrayPool<double>.Shared.Return(lBuf);
ArrayPool<int>.Shared.Return(hDeque);
ArrayPool<int>.Shared.Return(lDeque);
ArrayPool<long>.Shared.Return(hDeque);
ArrayPool<long>.Shared.Return(lDeque);
}
}
+25 -1
View File
@@ -11,7 +11,7 @@ namespace QuanTAlib;
/// Middle band is the JMA smoothed value itself.
/// </summary>
[SkipLocalsInit]
public sealed class Jbands : ITValuePublisher
public sealed class Jbands : ITValuePublisher, IDisposable
{
private const int VolWindowSize = 128;
private const int DevWindowSize = 10;
@@ -30,6 +30,10 @@ public sealed class Jbands : ITValuePublisher
private readonly RingBuffer _volBuffer;
private readonly TValuePublishedHandler _handler;
// Subscription tracking for IDisposable
private ITValuePublisher? _source;
private bool _disposed;
// Streaming state
private State _state;
private State _p_state;
@@ -114,9 +118,29 @@ public sealed class Jbands : ITValuePublisher
public Jbands(ITValuePublisher source, int period, int phase = 0, double power = 0.45)
: this(period, phase, power)
{
_source = source;
source.Pub += _handler;
}
/// <summary>
/// Releases the event subscription to the source publisher.
/// </summary>
public void Dispose()
{
if (_disposed)
{
return;
}
if (_source != null)
{
_source.Pub -= _handler;
_source = null;
}
_disposed = true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
+34 -5
View File
@@ -24,7 +24,7 @@ public enum MaenvType
/// Lower = Middle - (Middle × percentage / 100)
/// </summary>
[SkipLocalsInit]
public sealed class Maenv : ITValuePublisher
public sealed class Maenv : ITValuePublisher, IDisposable
{
private readonly int _period;
private readonly double _percentage;
@@ -60,6 +60,10 @@ public sealed class Maenv : ITValuePublisher
private readonly TValuePublishedHandler _valueHandler;
// Subscription tracking for IDisposable
private TSeries? _source;
private bool _disposed;
public string Name { get; }
public int WarmupPeriod { get; }
public TValue Last { get; private set; }
@@ -108,10 +112,30 @@ public sealed class Maenv : ITValuePublisher
public Maenv(TSeries source, int period = 20, double percentage = 1.0, MaenvType maType = MaenvType.EMA) : this(period, percentage, maType)
{
_source = source;
Prime(source);
source.Pub += _valueHandler;
}
/// <summary>
/// Releases the event subscription to the source publisher.
/// </summary>
public void Dispose()
{
if (_disposed)
{
return;
}
if (_source != null)
{
_source.Pub -= _valueHandler;
_source = null;
}
_disposed = true;
}
private void HandleValue(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -249,15 +273,20 @@ public sealed class Maenv : ITValuePublisher
var vUpperSpan = CollectionsMarshal.AsSpan(vUpper);
var vLowerSpan = CollectionsMarshal.AsSpan(vLower);
Batch(source.Values, vMiddleSpan, vUpperSpan, vLowerSpan, _period, _percentage, _maType);
// Process through streaming path to compute results and prime state in one pass
Reset();
for (int i = 0; i < len; i++)
{
Update(source[i], isNew: true);
vMiddleSpan[i] = Last.Value;
vUpperSpan[i] = Upper.Value;
vLowerSpan[i] = Lower.Value;
}
source.Times.CopyTo(tSpan);
tSpan.CopyTo(CollectionsMarshal.AsSpan(tUpper));
tSpan.CopyTo(CollectionsMarshal.AsSpan(tLower));
// Prime internal state for continued streaming
Prime(source);
var lastTime = new DateTime(source.Times[^1], DateTimeKind.Utc);
Last = new TValue(lastTime, vMiddleSpan[^1]);
Upper = new TValue(lastTime, vUpperSpan[^1]);
+14
View File
@@ -29,6 +29,8 @@ public sealed class Pchannel : ITValuePublisher
// Rolling counters
private int _count;
private long _index;
private int _p_count;
private long _p_index;
[StructLayout(LayoutKind.Auto)]
private record struct State(double LastValidHigh, double LastValidLow, bool IsHot);
@@ -197,6 +199,8 @@ public sealed class Pchannel : ITValuePublisher
if (isNew)
{
_p_state = _state;
_p_index = _index;
_p_count = _count;
_index++;
if (_count < _period)
{
@@ -206,6 +210,14 @@ public sealed class Pchannel : ITValuePublisher
else
{
_state = _p_state;
_index = _p_index;
_count = _p_count;
// Re-increment for current bar being reprocessed
_index++;
if (_count < _period)
{
_count++;
}
}
int bufIdx = (int)(_index % _period);
@@ -318,6 +330,8 @@ public sealed class Pchannel : ITValuePublisher
_lCount = 0;
_count = 0;
_index = -1;
_p_count = 0;
_p_index = -1;
_state = new State(double.NaN, double.NaN, false);
_p_state = _state;
Last = default;
+53 -7
View File
@@ -360,26 +360,60 @@ public sealed class Regchannel : ITValuePublisher
double sumX2Full = (period - 1.0) * period * (2.0 * period - 1.0) / 6.0;
double denomFull = period * sumX2Full - sumXFull * sumXFull;
// Track last valid value for NaN substitution
double lastValid = double.NaN;
for (int i = 0; i < len; i++)
{
// Get valid value with last-valid substitution
double currentValue = source[i];
if (double.IsFinite(currentValue))
{
lastValid = currentValue;
}
else
{
currentValue = lastValid;
}
// If still NaN (no valid value seen yet), output NaN
if (!double.IsFinite(currentValue))
{
middle[i] = double.NaN;
upper[i] = double.NaN;
lower[i] = double.NaN;
continue;
}
int count = Math.Min(i + 1, period);
int start = i - count + 1;
if (count <= 1)
{
middle[i] = source[i];
upper[i] = source[i];
lower[i] = source[i];
middle[i] = currentValue;
upper[i] = currentValue;
lower[i] = currentValue;
continue;
}
// Calculate sums for linear regression
// Calculate sums for linear regression with NaN handling
double sumY = 0;
double sumXY = 0;
double lastValidInWindow = double.NaN;
for (int j = 0; j < count; j++)
{
double y = source[start + j];
double rawY = source[start + j];
double y;
if (double.IsFinite(rawY))
{
lastValidInWindow = rawY;
y = rawY;
}
else
{
y = double.IsFinite(lastValidInWindow) ? lastValidInWindow : 0.0;
}
sumY += y;
sumXY += j * y;
}
@@ -414,12 +448,24 @@ public sealed class Regchannel : ITValuePublisher
regression = Math.FusedMultiplyAdd(slope, count - 1, intercept);
}
// Calculate standard deviation of residuals
// Calculate standard deviation of residuals with NaN handling
double sumResiduals2 = 0;
lastValidInWindow = double.NaN;
for (int j = 0; j < count; j++)
{
double rawY = source[start + j];
double y;
if (double.IsFinite(rawY))
{
lastValidInWindow = rawY;
y = rawY;
}
else
{
y = double.IsFinite(lastValidInWindow) ? lastValidInWindow : 0.0;
}
double predicted = Math.FusedMultiplyAdd(slope, j, intercept);
double residual = source[start + j] - predicted;
double residual = y - predicted;
sumResiduals2 = Math.FusedMultiplyAdd(residual, residual, sumResiduals2);
}
+24 -14
View File
@@ -311,21 +311,25 @@ public sealed class Starchannel : ITValuePublisher
double atrAlpha = 1.0 / period;
// SMA running sum
double smaSum = close[0];
// First bar - sanitize first values
double lastValidClose = double.IsFinite(close[0]) ? close[0] : 0;
double lastValidHigh = double.IsFinite(high[0]) ? high[0] : lastValidClose;
double lastValidLow = double.IsFinite(low[0]) ? low[0] : lastValidClose;
// SMA running sum (initialized with sanitized first close)
double smaSum = lastValidClose;
double rawRma = 0.0;
double e = 1.0;
double prevClose = close[0];
double prevClose = lastValidClose;
middle[0] = lastValidClose;
upper[0] = lastValidClose;
lower[0] = lastValidClose;
// First bar
middle[0] = close[0];
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];
// Track sanitized close values for SMA subtraction
// Use stackalloc for period-sized buffer to track sanitized values
Span<double> sanitizedCloseBuffer = period <= 256 ? stackalloc double[period] : new double[period];
sanitizedCloseBuffer[0] = lastValidClose;
int bufferHead = 1;
for (int i = 1; i < len; i++)
{
@@ -361,15 +365,21 @@ public sealed class Starchannel : ITValuePublisher
l = lastValidLow;
}
// SMA: add current, subtract oldest if beyond window
// SMA: add current sanitized value, subtract oldest sanitized value if beyond window
if (i < period)
{
smaSum += c;
}
else
{
smaSum += c - close[i - period];
// Subtract the sanitized value from period bars ago, not raw close
int oldIndex = bufferHead;
smaSum += c - sanitizedCloseBuffer[oldIndex];
}
// Store sanitized close in ring buffer
sanitizedCloseBuffer[bufferHead] = c;
bufferHead = (bufferHead + 1) % period;
int count = Math.Min(i + 1, period);
double sma = smaSum / count;
+4
View File
@@ -50,6 +50,7 @@ public sealed class Stbands : AbstractBase
private State _state;
private State _p_state;
private int _index;
private int _p_index;
public override bool IsHot => _index >= WarmupPeriod;
@@ -98,6 +99,7 @@ public sealed class Stbands : AbstractBase
private void Init()
{
_index = 0;
_p_index = 0;
_state = new State(0, 0, 1, 0, false);
_p_state = _state;
_trBuffer.Clear();
@@ -118,12 +120,14 @@ public sealed class Stbands : AbstractBase
if (isNew)
{
_p_state = _state;
_p_index = _index;
_index++;
}
else
{
// Restore previous state
_state = _p_state;
_index = _p_index;
}
double high = GetFiniteValue(input.High, _state.PrevClose);
+10 -2
View File
@@ -98,18 +98,26 @@ public class UbandsTests
double originalUpper = ubands.Upper.Value;
double originalLower = ubands.Lower.Value;
// Make multiple corrections
// Verify original values are finite
Assert.True(double.IsFinite(originalMiddle), $"Original middle should be finite: {originalMiddle}");
// Make multiple corrections - check each one
for (int i = 0; i < 10; i++)
{
ubands.Update(new TValue(DateTime.UtcNow, 150.0 + i), isNew: false);
Assert.True(double.IsFinite(ubands.Middle.Value),
$"Correction {i}: Middle should be finite, got {ubands.Middle.Value}");
}
// Restore original
// Restore original - verify input is finite
Assert.True(double.IsFinite(series[^1].Value), $"series[^1] should be finite: {series[^1].Value}");
ubands.Update(series[^1], isNew: false);
double restoredMiddle = ubands.Middle.Value;
double restoredUpper = ubands.Upper.Value;
double restoredLower = ubands.Lower.Value;
Assert.True(double.IsFinite(restoredMiddle), $"Restored middle should be finite: {restoredMiddle}");
Assert.Equal(originalMiddle, restoredMiddle, precision: 8);
Assert.Equal(originalUpper, restoredUpper, precision: 8);
Assert.Equal(originalLower, restoredLower, precision: 8);
+67 -59
View File
@@ -44,15 +44,13 @@ public sealed class Ubands : AbstractBase
double Usf2,
double PrevInput1,
double PrevInput2,
double LastValidValue,
int Count,
bool IsInitialized);
double LastPrice,
int Bars);
private State _state;
private State _p_state;
private int _index;
public override bool IsHot => _index >= WarmupPeriod;
public override bool IsHot => _state.Bars >= WarmupPeriod;
/// <summary>
/// Upper band (middle + mult × RMS)
@@ -113,93 +111,95 @@ public sealed class Ubands : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Init()
{
_index = 0;
_state = new State(0, 0, 0, 0, double.NaN, 0, false);
_p_state = _state;
_state = default;
_p_state = default;
_residualBuffer.Clear();
Upper = new TValue(DateTime.UtcNow, 0);
Middle = new TValue(DateTime.UtcNow, 0);
Lower = new TValue(DateTime.UtcNow, 0);
Width = new TValue(DateTime.UtcNow, 0);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private double GetFiniteValue(double value, bool isNew)
{
if (double.IsFinite(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;
Upper = default;
Middle = default;
Lower = default;
Width = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
private void HandleStateSnapshot(bool isNew)
{
// State management for bar correction
if (isNew)
{
_p_state = _state;
_index++;
_residualBuffer.Snapshot();
}
else
{
// Restore previous state
_state = _p_state;
_residualBuffer.Restore();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private (double usf, double upper, double lower) Step(double value, bool isNew)
{
HandleStateSnapshot(isNew);
// Handle NaN/Infinity input
if (!double.IsFinite(value))
{
if (_state.Bars == 0)
{
return (double.NaN, double.NaN, double.NaN);
}
value = _state.LastPrice;
}
else
{
_state.LastPrice = value;
}
double val = GetFiniteValue(input.Value, isNew);
_state.Bars++;
// Initialize on first value
if (!_state.IsInitialized)
// Initialize on first bar
if (_state.Bars == 1)
{
_state = _state with
{
Usf1 = val,
Usf2 = val,
PrevInput1 = val,
PrevInput2 = val,
Count = 1,
IsInitialized = true
};
_state.Usf1 = value;
_state.Usf2 = value;
_state.PrevInput1 = value;
_state.PrevInput2 = value;
return (value, value, value);
}
// Calculate USF (Ehlers Ultrasmooth Filter)
double usf;
if (_state.Count < 4)
if (_state.Bars < 4)
{
usf = val;
usf = value;
}
else
{
usf = Math.FusedMultiplyAdd(_c3, _state.Usf2,
Math.FusedMultiplyAdd(_c2, _state.Usf1,
Math.FusedMultiplyAdd(_k2, _state.PrevInput2,
Math.FusedMultiplyAdd(_k1, _state.PrevInput1, _k0 * val))));
Math.FusedMultiplyAdd(_k1, _state.PrevInput1, _k0 * value))));
// Guard against NaN propagation from state
if (!double.IsFinite(usf))
{
usf = value;
}
}
// Update USF state
_state = _state with
{
Usf2 = _state.Usf1,
Usf1 = usf,
PrevInput2 = _state.PrevInput1,
PrevInput1 = val,
Count = isNew ? _state.Count + 1 : _state.Count
};
_state.Usf2 = _state.Usf1;
_state.Usf1 = usf;
_state.PrevInput2 = _state.PrevInput1;
_state.PrevInput1 = value;
// Calculate residual and add to buffer
double residual = val - usf;
_residualBuffer.Add(residual * residual, isNew); // Store squared residual
double residual = value - usf;
_residualBuffer.Add(residual * residual); // Store squared residual
// Calculate RMS from squared residuals
double rms = _residualBuffer.Count > 0
? Math.Sqrt(_residualBuffer.Sum / _residualBuffer.Count)
// Use Max(0, Sum) to protect against floating-point drift making Sum slightly negative
double sumSq = _residualBuffer.Sum;
double rms = (_residualBuffer.Count > 0 && sumSq > 0)
? Math.Sqrt(sumSq / _residualBuffer.Count)
: 0;
// Calculate bands
@@ -207,6 +207,14 @@ public sealed class Ubands : AbstractBase
double upper = usf + bandOffset;
double lower = usf - bandOffset;
return (usf, upper, lower);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
var (usf, upper, lower) = Step(input.Value, isNew);
// Update output values
Upper = new TValue(input.Time, upper);
Middle = new TValue(input.Time, usf);
@@ -389,4 +397,4 @@ public sealed class Ubands : AbstractBase
lower[i] = usf - bandOffset;
}
}
}
}
+1 -1
View File
@@ -88,7 +88,7 @@ public sealed class Uchannel : AbstractBase
public TValue Width => new(Upper.Time, Upper.Value - Lower.Value);
/// <summary>
///
/// Initializes a new instance of Uchannel with specified parameters.
/// </summary>
/// <param name="strPeriod">Period for smoothing True Range. Must be >= 1.</param>
/// <param name="centerPeriod">Period for smoothing centerline. Must be >= 1.</param>
+4
View File
@@ -266,6 +266,8 @@ public sealed class Vwapbands : AbstractBase
throw new ArgumentNullException(nameof(source));
}
Reset();
int len = source.Count;
TSeries result = new(capacity: len);
@@ -288,6 +290,8 @@ public sealed class Vwapbands : AbstractBase
throw new ArgumentNullException(nameof(source));
}
Reset();
int len = source.Count;
TSeries result = new(capacity: len);