mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-18 02:28:05 +00:00
feat: Add Blackman Window Moving Average (BLMA) implementation and documentation
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public sealed class Blma : AbstractBase
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly RingBuffer _buffer;
|
||||
private readonly double[] _weights;
|
||||
private readonly double _weightSum;
|
||||
|
||||
public override bool IsHot => _buffer.Count >= _period;
|
||||
|
||||
public Blma(int period)
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0");
|
||||
}
|
||||
|
||||
_period = period;
|
||||
Name = $"Blma({period})";
|
||||
WarmupPeriod = period;
|
||||
_buffer = new RingBuffer(period);
|
||||
_weights = new double[period];
|
||||
|
||||
// Pre-calculate weights for the full period
|
||||
_weightSum = CalculateWeights(period, _weights);
|
||||
}
|
||||
|
||||
public Blma(object source, int period) : this(period)
|
||||
{
|
||||
var pub = (ITValuePublisher)source;
|
||||
pub.Pub += Handle;
|
||||
}
|
||||
|
||||
private void Handle(TValue value)
|
||||
{
|
||||
Update(value);
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
_buffer.Clear();
|
||||
}
|
||||
|
||||
public override void Prime(ReadOnlySpan<double> source)
|
||||
{
|
||||
foreach (var value in source)
|
||||
{
|
||||
Update(new TValue(DateTime.UtcNow, value));
|
||||
}
|
||||
}
|
||||
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (double.IsNaN(input.Value) || double.IsInfinity(input.Value))
|
||||
{
|
||||
return Last;
|
||||
}
|
||||
|
||||
_buffer.Add(input.Value, isNew);
|
||||
|
||||
double result;
|
||||
if (_buffer.Count < _period)
|
||||
{
|
||||
// During warmup, calculate weights dynamically for the current count
|
||||
int count = _buffer.Count;
|
||||
if (count == 1)
|
||||
{
|
||||
result = input.Value;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Full period, use pre-calculated weights
|
||||
result = CalculateWeightedSum(_buffer, _weights) / _weightSum;
|
||||
}
|
||||
|
||||
var tValue = new TValue(input.Time, result);
|
||||
Last = tValue;
|
||||
PubEvent(tValue);
|
||||
return tValue;
|
||||
}
|
||||
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
var result = new TSeries();
|
||||
Span<double> output = new double[source.Count];
|
||||
Calculate(source.Values, output, _period);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
result.Add(new TValue(source[i].Time, output[i]));
|
||||
}
|
||||
|
||||
// Restore state by replaying last Period bars
|
||||
// This ensures the indicator is ready for subsequent streaming updates
|
||||
Reset();
|
||||
int start = Math.Max(0, source.Count - _period);
|
||||
for (int i = start; i < source.Count; i++)
|
||||
{
|
||||
Update(source[i]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static double CalculateWeights(int n, Span<double> weights)
|
||||
{
|
||||
if (n == 1)
|
||||
{
|
||||
weights[0] = 1.0;
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
double totalWeight = 0;
|
||||
double invNMinus1 = 1.0 / (n - 1);
|
||||
double pi2 = 2.0 * Math.PI;
|
||||
double pi4 = 4.0 * Math.PI;
|
||||
|
||||
// Blackman window coefficients
|
||||
const double a0 = 0.42;
|
||||
const double a1 = 0.5;
|
||||
const double a2 = 0.08;
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
double ratio = i * invNMinus1;
|
||||
double w = a0 - (a1 * Math.Cos(pi2 * ratio)) + (a2 * Math.Cos(pi4 * ratio));
|
||||
weights[i] = w;
|
||||
totalWeight += w;
|
||||
}
|
||||
|
||||
return totalWeight;
|
||||
}
|
||||
|
||||
private static double CalculateWeightedSum(RingBuffer buffer, ReadOnlySpan<double> weights)
|
||||
{
|
||||
double sum = 0;
|
||||
for (int i = 0; i < buffer.Count; i++)
|
||||
{
|
||||
sum += buffer[i] * weights[i];
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> destination, int period)
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0");
|
||||
}
|
||||
|
||||
// Pre-calculate weights for full period
|
||||
Span<double> weights = period <= 256 ? stackalloc double[period] : new double[period];
|
||||
double weightSum = CalculateWeights(period, weights);
|
||||
|
||||
// Buffer for warmup weights to avoid stackalloc in loop
|
||||
Span<double> warmupWeightsBuffer = period <= 256 ? stackalloc double[period] : new double[period];
|
||||
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
int count = Math.Min(i + 1, period);
|
||||
|
||||
if (count < period)
|
||||
{
|
||||
// Warmup: dynamic weights
|
||||
if (count == 1)
|
||||
{
|
||||
destination[i] = source[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
Span<double> currentWeights = warmupWeightsBuffer.Slice(0, count);
|
||||
double currentWeightSum = CalculateWeights(count, currentWeights);
|
||||
|
||||
if (Math.Abs(currentWeightSum) < double.Epsilon)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
else
|
||||
{
|
||||
double sum = 0;
|
||||
for (int j = 0; j < count; j++)
|
||||
{
|
||||
sum += source[i - count + 1 + j] * currentWeights[j];
|
||||
}
|
||||
destination[i] = sum / currentWeightSum;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Full period
|
||||
double sum = 0;
|
||||
for (int j = 0; j < period; j++)
|
||||
{
|
||||
sum += source[i - period + 1 + j] * weights[j];
|
||||
}
|
||||
destination[i] = sum / weightSum;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user