mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-20 19:48:05 +00:00
Add Ultimate Oscillator implementation and documentation
- Introduced the Ultimate Oscillator (UltOsc) indicator with detailed mathematical foundation and performance profile. - Added historical context and common pitfalls for better user understanding. - Implemented Bilateral filter with enhanced update methods and batch calculations. - Updated Blackman Moving Average (BLMA) with improved handling of NaN values and batch processing capabilities. - Created unit tests for AmatIndicator to ensure proper functionality and signal generation. - Integrated AmatIndicator into the Quantower platform with appropriate line series for trend and strength visualization. - Updated project file to include new indicator implementations.
This commit is contained in:
@@ -31,7 +31,6 @@ public sealed class Bilateral : AbstractBase
|
||||
private record struct State(double SumSq, double LastValidValue);
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
private readonly TValuePublishedHandler _handler;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Bilateral Filter with specified parameters.
|
||||
@@ -50,7 +49,6 @@ public sealed class Bilateral : AbstractBase
|
||||
_buffer = new RingBuffer(period);
|
||||
Name = $"Bilateral({period}, {sigmaSRatio:F2}, {sigmaRMult:F2})";
|
||||
WarmupPeriod = period;
|
||||
_handler = Handle;
|
||||
|
||||
_spatialWeights = new double[period];
|
||||
PrecalculateSpatialWeights();
|
||||
@@ -59,7 +57,7 @@ public sealed class Bilateral : AbstractBase
|
||||
public Bilateral(ITValuePublisher source, int period, double sigmaSRatio = 0.5, double sigmaRMult = 1.0)
|
||||
: this(period, sigmaSRatio, sigmaRMult)
|
||||
{
|
||||
source.Pub += _handler;
|
||||
source.Pub += Handle;
|
||||
}
|
||||
|
||||
public override bool IsHot => _buffer.IsFull;
|
||||
@@ -142,6 +140,25 @@ public sealed class Bilateral : AbstractBase
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a new value.
|
||||
/// </summary>
|
||||
/// <param name="input">The input value with timestamp.</param>
|
||||
/// <param name="isNew">True for a new bar, false to update the current bar (intra-bar correction).</param>
|
||||
/// <returns>The calculated bilateral filter value.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Bar Correction Limitation:</b> For windowed indicators like Bilateral, the isNew=false
|
||||
/// behavior only corrects the most recent value in the buffer. It does NOT restore the full
|
||||
/// buffer state from before the last isNew=true call. This means multiple consecutive
|
||||
/// isNew=false calls work correctly, but the correction is limited to the current bar only.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// For scalar-state indicators (EMA, SMA running sum), full state rollback is possible.
|
||||
/// For buffer-based indicators, consider using Batch/Calculate methods for historical
|
||||
/// recalculation if perfect state restoration is required.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
@@ -212,7 +229,9 @@ public sealed class Bilateral : AbstractBase
|
||||
|
||||
// Variance = (SumSq - (Sum*Sum)/N) / N
|
||||
// Use Math.Max(0, ...) to handle potential floating point negative zero
|
||||
double variance = Math.Max(0, (_state.SumSq - (sum * sum) / count) / count);
|
||||
// Pre-compute inverse for efficiency
|
||||
double invCount = 1.0 / count;
|
||||
double variance = Math.Max(0, (_state.SumSq - sum * sum * invCount) * invCount);
|
||||
double stdev = Math.Sqrt(variance);
|
||||
|
||||
double sigmaR = Math.Max(stdev * _sigmaRMult, 1e-10);
|
||||
@@ -276,6 +295,19 @@ public sealed class Bilateral : AbstractBase
|
||||
Last = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates bilateral filter values for a TSeries and returns both results and a primed indicator.
|
||||
/// </summary>
|
||||
public static (TSeries Results, Bilateral Indicator) Calculate(TSeries source, int period, double sigmaSRatio = 0.5, double sigmaRMult = 1.0)
|
||||
{
|
||||
var indicator = new Bilateral(period, sigmaSRatio, sigmaRMult);
|
||||
var results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates bilateral filter values using spans (zero allocation in hot path).
|
||||
/// </summary>
|
||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> destination, int period, double sigmaSRatio = 0.5, double sigmaRMult = 1.0)
|
||||
{
|
||||
if (period <= 0)
|
||||
@@ -349,7 +381,8 @@ public sealed class Bilateral : AbstractBase
|
||||
if (count < period) count++;
|
||||
|
||||
// Calculate StDev
|
||||
double variance = Math.Max(0, (sumSq - (sum * sum) / count) / count);
|
||||
double invCount = 1.0 / count;
|
||||
double variance = Math.Max(0, (sumSq - sum * sum * invCount) * invCount);
|
||||
double stdev = Math.Sqrt(variance);
|
||||
|
||||
double sigmaR = Math.Max(stdev * sigmaRMult, 1e-10);
|
||||
@@ -380,4 +413,21 @@ public sealed class Bilateral : AbstractBase
|
||||
destination[i] = sumWeights < 1e-10 ? centerVal : sumWeightedSrc / sumWeights;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculates bilateral filter values for a TSeries.
|
||||
/// </summary>
|
||||
public static TSeries Batch(TSeries source, int period, double sigmaSRatio = 0.5, double sigmaRMult = 1.0)
|
||||
{
|
||||
var indicator = new Bilateral(period, sigmaSRatio, sigmaRMult);
|
||||
return indicator.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculates bilateral filter values using spans (zero allocation in hot path).
|
||||
/// </summary>
|
||||
public static void Batch(ReadOnlySpan<double> source, Span<double> destination, int period, double sigmaSRatio = 0.5, double sigmaRMult = 1.0)
|
||||
{
|
||||
Calculate(source, destination, period, sigmaSRatio, sigmaRMult);
|
||||
}
|
||||
}
|
||||
|
||||
+112
-57
@@ -1,18 +1,19 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public sealed class Blma : AbstractBase, IDisposable
|
||||
/// <summary>
|
||||
/// BLMA: Blackman Moving Average
|
||||
/// A weighted moving average using the Blackman window function for smoother transitions.
|
||||
/// </summary>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Blma : AbstractBase
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly RingBuffer _buffer;
|
||||
private readonly double[] _weights;
|
||||
private readonly double _weightSum;
|
||||
private readonly TValuePublishedHandler _handler;
|
||||
private ITValuePublisher? _publisher;
|
||||
private bool _hasLast;
|
||||
|
||||
public override bool IsHot => _buffer.Count >= _period;
|
||||
|
||||
@@ -31,24 +32,14 @@ public sealed class Blma : AbstractBase, IDisposable
|
||||
|
||||
// Pre-calculate weights for the full period
|
||||
_weightSum = CalculateWeights(period, _weights);
|
||||
_handler = Handle;
|
||||
}
|
||||
|
||||
public Blma(ITValuePublisher source, int period) : this(period)
|
||||
{
|
||||
_publisher = source;
|
||||
source.Pub += _handler;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_publisher != null)
|
||||
{
|
||||
_publisher.Pub -= _handler;
|
||||
_publisher = null;
|
||||
}
|
||||
source.Pub += Handle;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void Handle(object? sender, in TValueEventArgs args)
|
||||
{
|
||||
Update(args.Value, args.IsNew);
|
||||
@@ -57,7 +48,7 @@ public sealed class Blma : AbstractBase, IDisposable
|
||||
public override void Reset()
|
||||
{
|
||||
_buffer.Clear();
|
||||
_hasLast = false;
|
||||
Last = default;
|
||||
}
|
||||
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
@@ -81,12 +72,14 @@ public sealed class Blma : AbstractBase, IDisposable
|
||||
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (double.IsNaN(input.Value) || double.IsInfinity(input.Value))
|
||||
// Handle NaN/Infinity - return last result without changing state
|
||||
double val = input.Value;
|
||||
if (!double.IsFinite(val))
|
||||
{
|
||||
return _hasLast ? Last : default;
|
||||
return Last;
|
||||
}
|
||||
|
||||
_buffer.Add(input.Value, isNew);
|
||||
_buffer.Add(val, isNew);
|
||||
|
||||
double result;
|
||||
if (_buffer.Count < _period)
|
||||
@@ -95,31 +88,29 @@ public sealed class Blma : AbstractBase, IDisposable
|
||||
int count = _buffer.Count;
|
||||
if (count == 1)
|
||||
{
|
||||
result = input.Value;
|
||||
result = val;
|
||||
}
|
||||
else
|
||||
{
|
||||
Span<double> currentWeights = stackalloc double[count];
|
||||
double currentWeightSum = CalculateWeights(count, currentWeights);
|
||||
|
||||
// Fallback for cases where weights sum to zero (e.g. N=2)
|
||||
result = Math.Abs(currentWeightSum) < double.Epsilon
|
||||
? _buffer.Average()
|
||||
: CalculateWeightedSum(_buffer, currentWeights) / currentWeightSum;
|
||||
result = ComputeWeightedAverage(
|
||||
currentWeightSum,
|
||||
CalculateWeightedSum(_buffer, currentWeights),
|
||||
_buffer.Average());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Full period, use pre-calculated weights
|
||||
// Fallback for cases where weights sum to zero (e.g. N=2)
|
||||
result = Math.Abs(_weightSum) < double.Epsilon
|
||||
? _buffer.Average()
|
||||
: CalculateWeightedSum(_buffer, _weights) / _weightSum;
|
||||
result = ComputeWeightedAverage(
|
||||
_weightSum,
|
||||
CalculateWeightedSum(_buffer, _weights),
|
||||
_buffer.Average());
|
||||
}
|
||||
|
||||
var tValue = new TValue(input.Time, result);
|
||||
Last = tValue;
|
||||
_hasLast = true;
|
||||
PubEvent(tValue, isNew);
|
||||
return tValue;
|
||||
}
|
||||
@@ -147,6 +138,15 @@ public sealed class Blma : AbstractBase, IDisposable
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes weighted average with fallback for zero weight sum.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double ComputeWeightedAverage(double weightSum, double weightedSum, double fallbackAverage)
|
||||
{
|
||||
return Math.Abs(weightSum) < double.Epsilon ? fallbackAverage : weightedSum / weightSum;
|
||||
}
|
||||
|
||||
private static double CalculateWeights(int n, Span<double> weights)
|
||||
{
|
||||
if (n == 1)
|
||||
@@ -176,6 +176,7 @@ public sealed class Blma : AbstractBase, IDisposable
|
||||
return totalWeight;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double CalculateWeightedSum(RingBuffer buffer, ReadOnlySpan<double> weights)
|
||||
{
|
||||
int start = buffer.StartIndex;
|
||||
@@ -196,6 +197,19 @@ public sealed class Blma : AbstractBase, IDisposable
|
||||
return sum1 + sum2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates BLMA values for a TSeries and returns both results and a primed indicator.
|
||||
/// </summary>
|
||||
public static (TSeries Results, Blma Indicator) Calculate(TSeries source, int period)
|
||||
{
|
||||
var indicator = new Blma(period);
|
||||
var results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates BLMA values using spans (high-performance batch API).
|
||||
/// </summary>
|
||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> destination, int period)
|
||||
{
|
||||
if (period < 1)
|
||||
@@ -215,8 +229,29 @@ public sealed class Blma : AbstractBase, IDisposable
|
||||
// Buffer for warmup weights to avoid stackalloc in loop
|
||||
Span<double> warmupWeightsBuffer = period <= 256 ? stackalloc double[period] : new double[period];
|
||||
|
||||
// Handle NaN via last-valid-value substitution
|
||||
double lastValid = double.NaN;
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
if (double.IsFinite(source[i]))
|
||||
{
|
||||
lastValid = source[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (!double.IsFinite(val))
|
||||
{
|
||||
val = double.IsNaN(lastValid) ? 0 : lastValid;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastValid = val;
|
||||
}
|
||||
|
||||
int count = Math.Min(i + 1, period);
|
||||
|
||||
if (count < period)
|
||||
@@ -224,49 +259,69 @@ public sealed class Blma : AbstractBase, IDisposable
|
||||
// Warmup: dynamic weights
|
||||
if (count == 1)
|
||||
{
|
||||
destination[i] = source[i];
|
||||
destination[i] = val;
|
||||
}
|
||||
else
|
||||
{
|
||||
Span<double> currentWeights = warmupWeightsBuffer.Slice(0, count);
|
||||
double currentWeightSum = CalculateWeights(count, currentWeights);
|
||||
|
||||
if (Math.Abs(currentWeightSum) < double.Epsilon)
|
||||
double sum = 0;
|
||||
for (int j = 0; j < count; j++)
|
||||
{
|
||||
// Fallback for zero sum weights (e.g. N=2)
|
||||
double sum = 0;
|
||||
for (int j = 0; j < count; j++)
|
||||
{
|
||||
sum += source[i - count + 1 + j];
|
||||
}
|
||||
destination[i] = sum / count;
|
||||
int srcIdx = i - count + 1 + j;
|
||||
double srcVal = source[srcIdx];
|
||||
if (!double.IsFinite(srcVal)) srcVal = lastValid;
|
||||
sum += srcVal * currentWeights[j];
|
||||
}
|
||||
else
|
||||
|
||||
double avg = 0;
|
||||
for (int j = 0; j < count; j++)
|
||||
{
|
||||
double sum = source.Slice(i - count + 1, count).DotProduct(currentWeights);
|
||||
destination[i] = sum / currentWeightSum;
|
||||
int srcIdx = i - count + 1 + j;
|
||||
double srcVal = source[srcIdx];
|
||||
if (!double.IsFinite(srcVal)) srcVal = lastValid;
|
||||
avg += srcVal;
|
||||
}
|
||||
avg /= count;
|
||||
|
||||
destination[i] = ComputeWeightedAverage(currentWeightSum, sum, avg);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Full period
|
||||
if (Math.Abs(weightSum) < double.Epsilon)
|
||||
double sum = 0;
|
||||
double avg = 0;
|
||||
for (int j = 0; j < period; j++)
|
||||
{
|
||||
// Fallback for zero sum weights (e.g. N=2)
|
||||
double sum = 0;
|
||||
for (int j = 0; j < period; j++)
|
||||
{
|
||||
sum += source[i - period + 1 + j];
|
||||
}
|
||||
destination[i] = sum / period;
|
||||
}
|
||||
else
|
||||
{
|
||||
double sum = source.Slice(i - period + 1, period).DotProduct(weights);
|
||||
destination[i] = sum / weightSum;
|
||||
int srcIdx = i - period + 1 + j;
|
||||
double srcVal = source[srcIdx];
|
||||
if (!double.IsFinite(srcVal)) srcVal = lastValid;
|
||||
sum += srcVal * weights[j];
|
||||
avg += srcVal;
|
||||
}
|
||||
avg /= period;
|
||||
|
||||
destination[i] = ComputeWeightedAverage(weightSum, sum, avg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculates BLMA values for a TSeries.
|
||||
/// </summary>
|
||||
public static TSeries Batch(TSeries source, int period)
|
||||
{
|
||||
var indicator = new Blma(period);
|
||||
return indicator.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculates BLMA values using spans.
|
||||
/// </summary>
|
||||
public static void Batch(ReadOnlySpan<double> source, Span<double> destination, int period)
|
||||
{
|
||||
Calculate(source, destination, period);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user