mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 20:48:04 +00:00
volume indicators
This commit is contained in:
@@ -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];
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user