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
+1 -1
View File
@@ -329,7 +329,7 @@ public sealed class Bessel : AbstractBase
}
Last = new TValue(input.Time, filt);
PubEvent(Last);
PubEvent(Last, isNew);
return Last;
}
+17 -17
View File
@@ -9,8 +9,8 @@ public class BpfIndicatorTests
{
var indicator = new BpfIndicator();
Assert.Equal(40, indicator.LowerPeriod);
Assert.Equal(10, indicator.UpperPeriod);
Assert.Equal(10, indicator.LowerPeriod);
Assert.Equal(40, indicator.UpperPeriod);
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Equal("BPF - Bandpass Filter", indicator.Name);
@@ -21,7 +21,7 @@ public class BpfIndicatorTests
[Fact]
public void BpfIndicator_MinHistoryDepths_EqualsZero()
{
var indicator = new BpfIndicator { LowerPeriod = 20, UpperPeriod = 5 };
var indicator = new BpfIndicator { LowerPeriod = 5, UpperPeriod = 20 };
Assert.Equal(0, BpfIndicator.MinHistoryDepths);
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
@@ -30,17 +30,17 @@ public class BpfIndicatorTests
[Fact]
public void BpfIndicator_ShortName_IncludesParameters()
{
var indicator = new BpfIndicator { LowerPeriod = 40, UpperPeriod = 10 };
var indicator = new BpfIndicator { LowerPeriod = 10, UpperPeriod = 40 };
Assert.Contains("BPF", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("40", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("10", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("40", indicator.ShortName, StringComparison.Ordinal);
}
[Fact]
public void BpfIndicator_Initialize_CreatesInternalBpf()
{
var indicator = new BpfIndicator { LowerPeriod = 40, UpperPeriod = 10 };
var indicator = new BpfIndicator { LowerPeriod = 10, UpperPeriod = 40 };
// Initialize should not throw
indicator.Initialize();
@@ -52,7 +52,7 @@ public class BpfIndicatorTests
[Fact]
public void BpfIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new BpfIndicator { LowerPeriod = 40, UpperPeriod = 10 };
var indicator = new BpfIndicator { LowerPeriod = 10, UpperPeriod = 40 };
indicator.Initialize();
// Add historical data
@@ -71,7 +71,7 @@ public class BpfIndicatorTests
[Fact]
public void BpfIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new BpfIndicator { LowerPeriod = 40, UpperPeriod = 10 };
var indicator = new BpfIndicator { LowerPeriod = 10, UpperPeriod = 40 };
indicator.Initialize();
var now = DateTime.UtcNow;
@@ -87,7 +87,7 @@ public class BpfIndicatorTests
[Fact]
public void BpfIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new BpfIndicator { LowerPeriod = 40, UpperPeriod = 10 };
var indicator = new BpfIndicator { LowerPeriod = 10, UpperPeriod = 40 };
indicator.Initialize();
var now = DateTime.UtcNow;
@@ -110,7 +110,7 @@ public class BpfIndicatorTests
foreach (var source in sources)
{
var indicator = new BpfIndicator { LowerPeriod = 40, UpperPeriod = 10, Source = source };
var indicator = new BpfIndicator { LowerPeriod = 10, UpperPeriod = 40, Source = source };
indicator.Initialize();
var now = DateTime.UtcNow;
@@ -125,13 +125,13 @@ public class BpfIndicatorTests
[Fact]
public void BpfIndicator_Periods_CanBeChanged()
{
var indicator = new BpfIndicator { LowerPeriod = 40, UpperPeriod = 10 };
Assert.Equal(40, indicator.LowerPeriod);
Assert.Equal(10, indicator.UpperPeriod);
var indicator = new BpfIndicator { LowerPeriod = 10, UpperPeriod = 40 };
Assert.Equal(10, indicator.LowerPeriod);
Assert.Equal(40, indicator.UpperPeriod);
indicator.LowerPeriod = 60;
indicator.UpperPeriod = 20;
Assert.Equal(60, indicator.LowerPeriod);
Assert.Equal(20, indicator.UpperPeriod);
indicator.LowerPeriod = 20;
indicator.UpperPeriod = 60;
Assert.Equal(20, indicator.LowerPeriod);
Assert.Equal(60, indicator.UpperPeriod);
}
}
+4 -4
View File
@@ -7,11 +7,11 @@ namespace QuanTAlib;
[SkipLocalsInit]
public sealed class BpfIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Max Period (HP)", sortIndex: 1, 1, 2000, 1, 0)]
public int LowerPeriod { get; set; } = 40;
[InputParameter("Lower Period (HP)", sortIndex: 1, 1, 2000, 1, 0)]
public int LowerPeriod { get; set; } = 10;
[InputParameter("Min Period (LP)", sortIndex: 2, 1, 2000, 1, 0)]
public int UpperPeriod { get; set; } = 10;
[InputParameter("Upper Period (LP)", sortIndex: 2, 1, 2000, 1, 0)]
public int UpperPeriod { get; set; } = 40;
[IndicatorExtensions.DataSourceInput]
public SourceType Source { get; set; } = SourceType.Close;
+7
View File
@@ -62,6 +62,13 @@ public sealed class Bpf : AbstractBase
throw new ArgumentOutOfRangeException(nameof(upperPeriod), "Upper period must be >= 1");
}
if (lowerPeriod >= upperPeriod)
{
throw new ArgumentException(
$"Lower cutoff period ({lowerPeriod}) must be less than upper cutoff period ({upperPeriod}) for a valid passband.",
nameof(lowerPeriod));
}
LowerPeriod = lowerPeriod;
UpperPeriod = upperPeriod;
Name = $"BPF({lowerPeriod},{upperPeriod})";
+1 -1
View File
@@ -32,7 +32,7 @@ public sealed class Butter : AbstractBase
_period = period;
CalculateCoefficients();
Name = $"Butter({_period})";
WarmupPeriod = 2;
WarmupPeriod = 4 * period;
_handler = new TValuePublishedHandler(Handle);
Init();
}
+4
View File
@@ -94,6 +94,9 @@ public sealed class Elliptic : AbstractBase
_b2 = b2_val * gain_corr;
_a1 = a1_val;
_a2 = a2_val;
// Initialize LastValid to NaN so first non-finite input doesn't use uninitialized 0.0
_state.LastValid = double.NaN;
}
public Elliptic(ITValuePublisher source, int period) : this(period)
@@ -201,6 +204,7 @@ public sealed class Elliptic : AbstractBase
public override void Reset()
{
_state = default;
_state.LastValid = double.NaN;
_p_state = default;
Last = default;
}
+40 -14
View File
@@ -1,3 +1,4 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -235,6 +236,8 @@ public sealed class Gauss : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, double sigma)
{
const int StackallocThreshold = 256;
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output spans must be of equal length.", nameof(output));
@@ -242,25 +245,40 @@ public sealed class Gauss : AbstractBase
int kernelSize = (int)(2 * Math.Ceiling(3.0 * sigma) + 1);
// Precompute weights
Span<double> weights = stackalloc double[kernelSize];
double sum = 0;
int center = kernelSize / 2;
double twoSigmaSq = 2.0 * sigma * sigma;
// Use stackalloc for small kernels, ArrayPool for large ones to avoid stack overflow
double[]? rented = null;
scoped Span<double> weights;
scoped Span<double> stackBuffer = stackalloc double[Math.Min(kernelSize, StackallocThreshold)];
for (int i = 0; i < kernelSize; i++)
if (kernelSize <= StackallocThreshold)
{
double x = i - center;
double weight = Math.Exp(-(x * x) / twoSigmaSq);
weights[i] = weight;
sum += weight;
weights = stackBuffer.Slice(0, kernelSize);
}
else
{
rented = ArrayPool<double>.Shared.Rent(kernelSize);
weights = rented.AsSpan(0, kernelSize);
}
double invSum = 1.0 / sum;
for (int i = 0; i < kernelSize; i++)
try
{
weights[i] *= invSum;
}
double sum = 0;
int center = kernelSize / 2;
double twoSigmaSq = 2.0 * sigma * sigma;
for (int i = 0; i < kernelSize; i++)
{
double x = i - center;
double weight = Math.Exp(-(x * x) / twoSigmaSq);
weights[i] = weight;
sum += weight;
}
double invSum = 1.0 / sum;
for (int i = 0; i < kernelSize; i++)
{
weights[i] *= invSum;
}
// Apply filter
for (int i = 0; i < source.Length; i++)
@@ -309,6 +327,14 @@ public sealed class Gauss : AbstractBase
output[i] = double.NaN;
}
}
}
finally
{
if (rented != null)
{
ArrayPool<double>.Shared.Return(rented, clearArray: false);
}
}
}
/// <summary>
+4 -2
View File
@@ -38,6 +38,8 @@ public sealed class Kalman : AbstractBase
private readonly ITValuePublisher? _publisher;
private readonly TValuePublishedHandler? _handler;
private const double MaxCovariance = 1e10;
private State _state;
private State _pState;
@@ -138,7 +140,7 @@ public sealed class Kalman : AbstractBase
}
else
{
_state.P += ProcessNoise;
_state.P = Math.Min(_state.P + ProcessNoise, MaxCovariance);
Last = new TValue(input.Time, _state.X);
}
@@ -243,7 +245,7 @@ public sealed class Kalman : AbstractBase
}
else
{
p += q; // predict-only
p = Math.Min(p + q, MaxCovariance); // predict-only, capped
output[i] = x;
}
continue;
+5 -19
View File
@@ -147,27 +147,13 @@ public sealed class Notch : AbstractBase
if (srcSpan.Length > 0)
{
_index += srcSpan.Length;
// Best effort state restoration from the end of the block
// We assume the strict history for X is valid.
double lastVal = srcSpan[^1];
_state.LastValue = lastVal;
if (srcSpan.Length >= 2)
// Replay last few bars through streaming Update to properly restore state
int replayStart = Math.Max(0, srcSpan.Length - Math.Max(WarmupPeriod, 4));
Reset();
for (int i = replayStart; i < srcSpan.Length; i++)
{
_state.X1 = srcSpan[^1];
_state.X2 = srcSpan[^2];
_state.Y1 = outArray[^1];
_state.Y2 = outArray[^2];
Update(new TValue(source.Times[i], srcSpan[i]), isNew: true);
}
else
{
_state.X2 = _state.X1;
_state.X1 = srcSpan[0];
_state.Y2 = _state.Y1;
_state.Y1 = outArray[0];
}
_p_state = _state;
}
return result;
+4 -2
View File
@@ -78,7 +78,8 @@ public sealed class Sgf : AbstractBase
}
else
{
weight = 1.0 - Math.Abs((double)k) / (double)halfWindow;
// Guard against division by zero when halfWindow == 0 (period == 1)
weight = (halfWindow == 0) ? 1.0 : 1.0 - Math.Abs((double)k) / (double)halfWindow;
}
_weights[i] = weight;
@@ -282,7 +283,8 @@ public sealed class Sgf : AbstractBase
}
else
{
weight = 1.0 - Math.Abs((double)k) / (double)halfWindow;
// Guard against division by zero when halfWindow == 0 (period == 1)
weight = (halfWindow == 0) ? 1.0 : 1.0 - Math.Abs((double)k) / (double)halfWindow;
}
weights[i] = weight;
+2 -2
View File
@@ -77,13 +77,13 @@ public sealed class Usf : AbstractBase
public Usf(TSeries source, int period) : this(period)
{
_publisher = source;
source.Pub += _handler;
Prime(source.Values);
if (source.Count > 0)
{
Last = new TValue(source.LastTime, Last.Value);
}
_publisher = source;
source.Pub += _handler;
}
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
+8 -4
View File
@@ -40,8 +40,12 @@ public sealed class Wiener : AbstractBase
{
if (double.IsNaN(input.Value) || double.IsInfinity(input.Value))
{
// If we have a valid last value, return it, otherwise return input
return isNew ? Last : new TValue(input.Time, Last.Value);
// If we have a valid last value, use it; otherwise fallback to input value
double fallbackValue = double.IsFinite(Last.Value) ? Last.Value : input.Value;
var fallbackResult = new TValue(input.Time, fallbackValue);
Last = fallbackResult;
PubEvent(fallbackResult, isNew);
return fallbackResult;
}
_buffer.Add(input.Value, isNew);
@@ -55,9 +59,9 @@ public sealed class Wiener : AbstractBase
return res;
}
double result = Calc();
double calcResult = Calc();
var ret = new TValue(input.Time, result);
var ret = new TValue(input.Time, calcResult);
Last = ret;
PubEvent(ret, isNew);
return ret;