9.7 KiB
BLMA: Blackman Window Moving Average
| Property | Value |
|---|---|
| Category | Trend (FIR MA) |
| Inputs | Source (close) |
| Parameters | period |
| Outputs | Single series (Blma) |
| Output range | Tracks input |
| Warmup | period bars |
| Signature | blma_signature |
TL;DR
- The Blackman Window Moving Average (BLMA) applies a triple-cosine window function from digital signal processing to financial time series.
- Parameterized by
period. - Output range: Tracks input.
- Requires
periodbars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available.
"If you want to filter noise, don't just average it - window it."
The Blackman Window Moving Average (BLMA) applies a triple-cosine window function from digital signal processing to financial time series. Originally developed by Ralph Beebe Blackman at Bell Labs in the 1950s for spectral analysis, this filter provides superior noise suppression compared to standard moving averages by minimizing spectral leakage.
Historical Context
In the early days of signal processing, engineers struggled with spectral leakage where energy from one frequency bleeds into others during analysis. Simple rectangular windows (like SMA) caused significant leakage. Blackman proposed a window function with tapered edges that drastically reduced this effect. In trading, "leakage" manifests as market noise distorting the trend signal. BLMA adapts this DSP innovation to create a trend filter that is remarkably smooth yet responsive to significant moves.
Architecture & Physics
BLMA is a Finite Impulse Response (FIR) filter. Unlike Exponential Moving Averages (IIR) which have infinite memory, BLMA considers only the last N bars.
The "physics" of BLMA relies on its bell-shaped weighting curve. The weights are highest in the center of the window and taper to zero at both ends (newest and oldest data). This symmetry means BLMA has a lag of approximately N/2, but it effectively suppresses high-frequency noise (jitter) that often plagues other averages.
The Zero-Edge Effect
Because the Blackman window tapers to zero at the edges (w[0] \approx 0 and w[N-1] \approx 0), the most recent price data has very little immediate impact on the indicator value. This creates a "smoothness" that filters out sudden spikes, but it also introduces a specific type of lag where the indicator is slow to react to a sudden trend reversal until the price move enters the "fat" part of the window (the center).
Mathematical Foundation
The Blackman window weights w(n) for a period N are calculated as:
w(n) = 0.42 - 0.5 \cos\left(\frac{2\pi n}{N-1}\right) + 0.08 \cos\left(\frac{4\pi n}{N-1}\right)
Where 0 \le n \le N-1.
The BLMA value is the weighted average:
BLMA_t = \frac{\sum_{i=0}^{N-1} P_{t-i} \cdot w(i)}{\sum_{i=0}^{N-1} w(i)}
Performance Profile
Operation Count (Streaming Mode, Scalar)
Constructor (one-time weight precomputation):
| Operation | Count | Cost (cycles) | Subtotal |
|---|---|---|---|
| COS | 2N | 40 | 80N |
| MUL | 4N | 3 | 12N |
| ADD/SUB | 3N | 1 | 3N |
| Total (init) | — | — | ~95N cycles |
For period=20: ~1,900 cycles (one-time).
Hot path (per bar):
| Operation | Count | Cost (cycles) | Subtotal |
|---|---|---|---|
| MUL | N | 3 | 3N |
| ADD | N | 1 | N |
| DIV | 1 | 15 | 15 |
| Total | 2N + 1 | — | ~4N + 15 cycles |
For period=20: ~95 cycles per bar.
Hot path breakdown:
- Weighted sum:
∑(buffer[i] × weights[i])→ N MUL + N ADD - Normalization:
sum / wSum→ 1 DIV (wSum precomputed)
Batch Mode (SIMD)
The convolution is highly vectorizable:
| Operation | Scalar Ops | SIMD Ops (AVX2) | Speedup |
|---|---|---|---|
| Weighted products | N | N/8 | 8× |
| Horizontal sum | N | log₂(8) | ~N/3× |
Batch efficiency (512 bars, period=20):
| Mode | Cycles/bar | Total | Notes |
|---|---|---|---|
| Scalar streaming | ~95 | ~48,640 | O(N) per bar |
| SIMD batch | ~25 | ~12,800 | Vectorized dot product |
| Improvement | ~4× | ~36K saved | — |
Quality Metrics
| Metric | Score | Notes |
|---|---|---|
| Accuracy | 10/10 | Precise DSP windowing |
| Timeliness | 4/10 | Significant lag (N/2) due to symmetric window |
| Overshoot | 10/10 | Never overshoots (FIR property) |
| Smoothness | 10/10 | Excellent noise suppression (-58dB side-lobes) |
Zero-Allocation Design
The implementation uses a pre-calculated weights array and a circular buffer (RingBuffer) to store price history. The Update method performs the weighted sum without allocating any new memory on the heap. For the static Calculate method, stackalloc is used for weights and temporary buffers for small periods (up to 256), ensuring high performance.
Validation
BLMA is validated against a reference implementation using the standard Blackman window formula.
| Library | Status | Notes |
|---|---|---|
| QuanTAlib | ✅ | Matches theoretical formula. |
| PineScript | ✅ | Matches PineScript reference logic. |
C# Implementation Considerations
The QuanTAlib BLMA implementation emphasizes precomputation and zero-allocation streaming:
Precomputed Weights Array
Blackman window weights are calculated once in the constructor and reused for every update:
public Blma(int period)
{
_weights = new double[period];
_weightSum = CalculateWeights(period, _weights);
}
private static double CalculateWeights(int n, Span<double> weights)
{
const double a0 = 0.42;
const double a1 = 0.5;
const double a2 = 0.08;
double invNMinus1 = 1.0 / (n - 1);
for (int i = 0; i < n; i++)
{
double ratio = i * invNMinus1;
double w = a0 - (a1 * Math.Cos(2.0 * Math.PI * ratio))
+ (a2 * Math.Cos(4.0 * Math.PI * ratio));
weights[i] = w;
totalWeight += w;
}
return totalWeight;
}
RingBuffer with DotProduct Extension
The weighted sum uses an optimized dot product that handles circular buffer wraparound:
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double CalculateWeightedSum(RingBuffer buffer, ReadOnlySpan<double> weights)
{
int start = buffer.StartIndex;
int count = buffer.Count;
int capacity = buffer.Capacity;
if (start + count <= capacity)
{
// Contiguous case - single dot product
return buffer.InternalBuffer.Slice(start, count).DotProduct(weights);
}
// Wraparound case - two dot products
int firstPartLength = capacity - start;
int secondPartLength = count - firstPartLength;
double sum1 = buffer.InternalBuffer.Slice(start, firstPartLength).DotProduct(weights[..firstPartLength]);
double sum2 = buffer.InternalBuffer.Slice(0, secondPartLength).DotProduct(weights[firstPartLength..]);
return sum1 + sum2;
}
Dynamic Warmup Weights
During warmup (fewer than period bars), weights are calculated dynamically using stackalloc:
if (_buffer.Count < _period)
{
int count = _buffer.Count;
Span<double> currentWeights = stackalloc double[count];
double currentWeightSum = CalculateWeights(count, currentWeights);
result = ComputeWeightedAverage(currentWeightSum, weightedSum, _buffer.Average());
}
Stackalloc Strategy for Batch Processing
The static Calculate method uses stackalloc for small periods (≤256) to avoid heap allocation:
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];
NaN Handling with Last-Valid-Value Substitution
Invalid values are substituted with the last valid value to maintain calculation continuity:
double val = input.Value;
if (!double.IsFinite(val))
{
return Last; // Return last result without changing state
}
In batch mode:
double lastValid = double.NaN;
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;
// ...
}
AggressiveInlining on Hot Paths
Critical methods are marked for inlining:
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double ComputeWeightedAverage(double weightSum, double weightedSum, double fallbackAverage)
{
return Math.Abs(weightSum) < double.Epsilon ? fallbackAverage : weightedSum / weightSum;
}
Memory Layout
| Field | Type | Size | Purpose |
|---|---|---|---|
_period |
int |
4 | Window size |
_buffer |
RingBuffer |
8 (ref) | Circular price storage |
_weights |
double[] |
8 (ref) | Precomputed Blackman weights |
_weightSum |
double |
8 | Sum of weights (precomputed) |
| Total | ~28 bytes | Per instance (excluding buffer/array internals) |
Weight array storage: period × 8 bytes (e.g., 160 bytes for period=20)
Common Pitfalls
- Lag: BLMA has more lag than EMA or WMA because it suppresses the most recent data. It is a smoothing filter, not a leading indicator.
- Warmup: During the first
Nbars, the window expands dynamically. The full noise-suppression characteristics are only achieved afterNbars.