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:
Miha Kralj
2025-12-31 23:30:54 -08:00
parent a42c9acd0b
commit 11f4ec2497
18 changed files with 3471 additions and 66 deletions
+55 -5
View File
@@ -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
View File
@@ -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);
}
}