mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-24 21:48:03 +00:00
Enhance documentation and improve validation in AFIRMA, ALMA, and Bessel implementations
This commit is contained in:
+45
-38
@@ -4,35 +4,35 @@ using System.Runtime.InteropServices;
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// AFIRMA: Autoregressive Finite Impulse Response Moving Average
|
||||
/// A hybrid filter combining ARMA modeling, FIR filtering, and cubic spline fitting.
|
||||
/// Provides superior noise reduction while maintaining signal fidelity and reducing lag.
|
||||
/// AFIRMA: Adaptive FIR Moving Average (Windowed Sinc Filter)
|
||||
/// A high-quality FIR low-pass filter using windowed sinc coefficients for
|
||||
/// optimal frequency response and superior noise reduction.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// AFIRMA combines three components:
|
||||
/// AFIRMA implements a Finite Impulse Response (FIR) filter using the mathematically
|
||||
/// optimal sinc function—the ideal low-pass filter impulse response—tempered by
|
||||
/// window functions to minimize spectral leakage.
|
||||
///
|
||||
/// 1. ARMA Component:
|
||||
/// X_t = c + ε_t + Σφ_i·X_{t-i} + Σθ_j·ε_{t-j}
|
||||
/// Provides autoregressive modeling of the time series.
|
||||
///
|
||||
/// 2. FIR Component:
|
||||
/// y[n] = Σb_i·x[n-i]
|
||||
/// Digital filter with windowed sinc coefficients for frequency-selective smoothing.
|
||||
///
|
||||
/// 3. Cubic Spline Fitting:
|
||||
/// Applied to most recent bars using least-squares polynomial fitting.
|
||||
/// Ensures smooth transition between filtered data and recent price movements.
|
||||
/// The filter equation:
|
||||
/// y[n] = Σ w_k · x[n-k] where w_k = Window(k) · sinc(π(k-c)/P)
|
||||
///
|
||||
/// Key features:
|
||||
/// - Windowed sinc filter for optimal frequency response
|
||||
/// - Supports Rectangular, Hanning, Hamming, Blackman, and Blackman-Harris windows
|
||||
/// - Least-squares cubic polynomial fitting for reduced lag at the leading edge
|
||||
/// - O(n) per update where n = taps
|
||||
/// - Blackman-Harris provides -92 dB sidelobe suppression for maximum noise rejection
|
||||
/// - O(taps) per update with SIMD-optimized batch processing
|
||||
///
|
||||
/// Window Functions and Sidelobe Suppression:
|
||||
/// - Rectangular: -13 dB (maximum frequency resolution, high leakage)
|
||||
/// - Hanning: -31 dB (general purpose smoothing)
|
||||
/// - Hamming: -42 dB (reduced leakage with decent resolution)
|
||||
/// - Blackman: -58 dB (low leakage, good for noisy data)
|
||||
/// - Blackman-Harris: -92 dB (minimum leakage, maximum smoothing)
|
||||
///
|
||||
/// Parameters:
|
||||
/// - Period: Affects overall smoothness of the indicator
|
||||
/// - Taps: Filter length, influences filter complexity
|
||||
/// - Window: Type of window function applied to sinc filter
|
||||
/// - Period: Controls cutoff frequency. Higher values = more smoothing.
|
||||
/// - Taps: Filter length. More taps = sharper frequency response but more lag.
|
||||
/// - Window: Type of window function applied to sinc filter.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Afirma : AbstractBase
|
||||
@@ -244,24 +244,27 @@ public sealed class Afirma : AbstractBase
|
||||
int count = _buffer.Count;
|
||||
if (count == 0) return double.NaN;
|
||||
|
||||
double result = 0.0;
|
||||
for (int k = 0; k < count; k++)
|
||||
{
|
||||
result += _buffer[k] * _weights[k];
|
||||
}
|
||||
|
||||
// Warmup path: calculate both sum and effective weight sum in single pass
|
||||
if (count < _taps)
|
||||
{
|
||||
// During warmup, adjust weight sum for partial buffer
|
||||
double result = 0.0;
|
||||
double effectiveWeightSum = 0.0;
|
||||
for (int k = 0; k < count; k++)
|
||||
{
|
||||
effectiveWeightSum += _weights[k];
|
||||
double w = _weights[k];
|
||||
result = Math.FusedMultiplyAdd(_buffer[k], w, result);
|
||||
effectiveWeightSum += w;
|
||||
}
|
||||
return effectiveWeightSum > 0 ? result / effectiveWeightSum : _buffer.Newest;
|
||||
}
|
||||
|
||||
return result * _invWeightSum;
|
||||
// Steady state: use pre-computed inverse weight sum
|
||||
double sum = 0.0;
|
||||
for (int k = 0; k < _taps; k++)
|
||||
{
|
||||
sum = Math.FusedMultiplyAdd(_buffer[k], _weights[k], sum);
|
||||
}
|
||||
return sum * _invWeightSum;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
@@ -320,6 +323,7 @@ public sealed class Afirma : AbstractBase
|
||||
|
||||
/// <summary>
|
||||
/// Calculates AFIRMA in-place, writing results to pre-allocated output span.
|
||||
/// Optimized with stackalloc and FMA.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period, int taps = 6, WindowType window = WindowType.BlackmanHarris)
|
||||
@@ -334,8 +338,14 @@ public sealed class Afirma : AbstractBase
|
||||
int len = source.Length;
|
||||
if (len == 0) return;
|
||||
|
||||
// Calculate weights once
|
||||
double[] weights = new double[taps];
|
||||
const int StackAllocThreshold = 256;
|
||||
|
||||
// Allocate weights with stackalloc to avoid heap allocation
|
||||
Span<double> weights = taps <= StackAllocThreshold
|
||||
? stackalloc double[taps]
|
||||
: new double[taps];
|
||||
|
||||
// Pre-calculate weights
|
||||
double centerTap = (taps - 1) / 2.0;
|
||||
int tapsMinusOne = taps - 1;
|
||||
double weightSum = 0.0;
|
||||
@@ -345,20 +355,17 @@ public sealed class Afirma : AbstractBase
|
||||
double windowWeight = GetWindowWeightStatic(k, tapsMinusOne, window);
|
||||
double x = Math.PI * (k - centerTap) / period;
|
||||
double sincWeight = Math.Abs(x) < 1e-10 ? 1.0 : Math.Sin(x) / x;
|
||||
|
||||
weights[k] = windowWeight * sincWeight;
|
||||
weightSum += weights[k];
|
||||
}
|
||||
|
||||
// Allocate buffer
|
||||
const int StackAllocThreshold = 256;
|
||||
// Allocate circular buffer with stackalloc
|
||||
Span<double> buffer = taps <= StackAllocThreshold
|
||||
? stackalloc double[taps]
|
||||
: new double[taps];
|
||||
|
||||
// Find first valid value for NaN handling
|
||||
double lastValid = double.NaN;
|
||||
|
||||
// Find first valid value
|
||||
for (int k = 0; k < len; k++)
|
||||
{
|
||||
if (double.IsFinite(source[k]))
|
||||
@@ -384,7 +391,7 @@ public sealed class Afirma : AbstractBase
|
||||
bufferIndex = (bufferIndex + 1) % taps;
|
||||
if (bufferCount < taps) bufferCount++;
|
||||
|
||||
// Calculate weighted sum
|
||||
// Calculate weighted sum using FMA
|
||||
double result = 0.0;
|
||||
double effectiveWeightSum = 0.0;
|
||||
int readIndex = (bufferIndex - bufferCount + taps) % taps;
|
||||
@@ -392,7 +399,7 @@ public sealed class Afirma : AbstractBase
|
||||
for (int k = 0; k < bufferCount; k++)
|
||||
{
|
||||
int idx = (readIndex + k) % taps;
|
||||
result += buffer[idx] * weights[k];
|
||||
result = Math.FusedMultiplyAdd(buffer[idx], weights[k], result);
|
||||
effectiveWeightSum += weights[k];
|
||||
}
|
||||
|
||||
|
||||
+36
-22
@@ -1,28 +1,43 @@
|
||||
# AFIRMA: Autoregressive Finite Impulse Response Moving Average
|
||||
# AFIRMA: Adaptive FIR Moving Average
|
||||
|
||||
> "When ARMA met FIR at a signal processing conference and they had a baby with cubic spline DNA. The result filters noise like a surgeon and tracks price like a stalker."
|
||||
> "When engineers realized that the mathematically perfect filter requires infinite memory, they reached for window functions—the art of graceful compromise between theory and reality."
|
||||
|
||||
AFIRMA is a hybrid smoothing filter that combines three signal processing techniques: autoregressive (AR) modeling, finite impulse response (FIR) filtering with windowed sinc coefficients, and cubic spline fitting for the leading edge. The result is a filter that achieves superior noise reduction while maintaining signal fidelity and minimizing lag.
|
||||
AFIRMA is a high-quality FIR (Finite Impulse Response) low-pass filter using windowed sinc coefficients. It attempts to solve the fundamental paradox of technical analysis: the inverse relationship between smoothness and timeliness. The sinc function represents the theoretically optimal low-pass filter, but it extends to infinity. AFIRMA truncates it using window functions to create a practical, finite-length filter with excellent noise rejection.
|
||||
|
||||
AFIRMA does not offer "vision." It offers a convolution engine that trades CPU cycles for signal fidelity.
|
||||
|
||||
## Historical Context
|
||||
|
||||
AFIRMA emerged from the intersection of econometric time series analysis (ARMA models from Box-Jenkins methodology, circa 1970) and digital signal processing (FIR filters with window functions). The combination addresses a fundamental problem: traditional moving averages either lag badly (SMA, EMA) or introduce ringing artifacts (sharp cutoff filters). AFIRMA uses the mathematically optimal sinc function—the ideal low-pass filter impulse response—tempered by window functions that trade off main lobe width against sidelobe suppression.
|
||||
In the 1970s, Box and Jenkins formalized ARMA models for econometrics. Simultaneously, digital signal processing (DSP) engineers were perfecting FIR filters using window functions to chop infinite Sinc waves into usable finite buffers.
|
||||
|
||||
The two worlds rarely spoke. Economists accepted lag; engineers accepted latency.
|
||||
|
||||
AFIRMA is a modern synthesis. It acknowledges that financial time series data is neither a pure radio wave nor a predictable economic cycle. It is a noisy, non-stationary mess. Traditional Moving Averages (SMA, EMA) use simple averaging which leaks high-frequency noise (lag) or reacts too violently (overshoot). AFIRMA uses the mathematically optimal Sinc function—the theoretical limit of a perfect low-pass filter—tempered by window functions to exist in reality.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
AFIRMA operates through a convolution of the input signal with pre-computed windowed sinc coefficients.
|
||||
AFIRMA operates through a convolution of the input signal with pre-computed windowed sinc coefficients. It is not a recursive loop (like EMA); it is a sliding weighted ruler.
|
||||
|
||||
### The Sinc Function
|
||||
### The Physics of the Sinc
|
||||
|
||||
The sinc function is the impulse response of an ideal low-pass filter:
|
||||
The heart of the filter is the normalized sinc function:
|
||||
|
||||
$$ \text{sinc}(x) = \begin{cases} 1 & \text{if } x = 0 \\ \frac{\sin(x)}{x} & \text{otherwise} \end{cases} $$
|
||||
$$ \text{sinc}(x) = \frac{\sin(\pi x)}{\pi x} $$
|
||||
|
||||
In practice, the sinc function extends infinitely—inconvenient for real-time processing. AFIRMA truncates it to a finite number of taps and applies a window function to minimize the resulting spectral leakage.
|
||||
In the frequency domain, this is a brick wall: it passes everything below a certain frequency and kills everything above it. Perfect.
|
||||
|
||||
### Window Functions
|
||||
**The catch:** To achieve this perfection in the time domain, the sinc function must extend from negative infinity to positive infinity. Since systems do not have infinite RAM or a time machine, the function must be truncated.
|
||||
|
||||
Window functions control the trade-off between frequency resolution (main lobe width) and spectral leakage (sidelobe suppression).
|
||||
### The Windowing Compromise
|
||||
|
||||
Chopping a sinc function abruptly (a "Rectangular" window) causes the Gibbs phenomenon—ringing artifacts where the filter oscillates wildly around sharp price changes. To prevent this, a "Window Function" gently tapers the edges of the filter to zero.
|
||||
|
||||
This is a trade-off:
|
||||
|
||||
1. **Main Lobe Width:** Determines frequency resolution (sharpness).
|
||||
2. **Sidelobe Amplitude:** Determines spectral leakage (noise suppression).
|
||||
|
||||
You cannot optimize both simultaneously. This is the Heisenberg uncertainty principle applied to moving averages.
|
||||
|
||||
| Window | Main Lobe | Sidelobe | Use Case |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
@@ -34,10 +49,6 @@ Window functions control the trade-off between frequency resolution (main lobe w
|
||||
|
||||
The default Blackman-Harris window provides the best sidelobe suppression, making AFIRMA robust to impulsive noise in price data.
|
||||
|
||||
### Cubic Spline Component
|
||||
|
||||
The ARMA polynomial coefficients are precomputed during initialization to support least-squares cubic fitting at the leading edge. This reduces end-point distortion common in FIR filters, where the filter "sees" incomplete data at the boundaries.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### 1. Windowed Sinc Coefficients
|
||||
@@ -160,16 +171,19 @@ For the same Period and Taps, different windows produce different smoothing char
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Too Many Taps**: More taps mean more lag. Don't use 50 taps "just because." Start with 5-9.
|
||||
1. **Tap Inflation:** There is a temptation to set `Taps = 50` thinking it provides "more accuracy." It provides more lag. Keep taps between 5 and 15 for trading. If you need 50 taps, you don't need a filter; you need a weekly chart.
|
||||
|
||||
2. **Period vs. Taps Confusion**: Period controls smoothness (like EMA period). Taps control filter sharpness. They're independent parameters.
|
||||
2. **Period vs. Taps Confusion:**
|
||||
- **Period** is the *what* (which frequencies to remove).
|
||||
- **Taps** is the *how* (how much math to throw at the removal).
|
||||
- Increasing Taps without changing Period just makes the filter steeper, not smoother.
|
||||
|
||||
3. **Rectangular Window**: Almost never the right choice for financial data. The severe sidelobe leakage introduces ringing.
|
||||
3. **The "Cold Start" Reality:** AFIRMA is an FIR filter. It requires `Taps` number of bars to fill its buffer. The first `Taps-1` values are approximations. Check `.IsHot` before trading real money.
|
||||
|
||||
4. **Cold Values**: AFIRMA needs `taps` bars of history to be fully warmed up. The `IsHot` property indicates when the filter is primed.
|
||||
4. **Rectangular Windows:** Do not use the Rectangular window unless you enjoy seeing price oscillations that don't exist. The severe sidelobe leakage (-13 dB) introduces ringing artifacts around sharp price changes.
|
||||
|
||||
## See Also
|
||||
|
||||
- [ALMA](../alma/Alma.md) - Gaussian-weighted moving average with offset
|
||||
- [CONV](../conv/Conv.md) - General convolution filter
|
||||
- [SSF](../ssf/Ssf.md) - Ehlers Super Smooth Filter (2-pole IIR)
|
||||
- [ALMA](../alma/Alma.md) - Arnaud Legoux's Gaussian approach (similar goal, different math)
|
||||
- [JMA](../jma/Jma.md) - Jurik's proprietary-turned-open filter (often slower, high overshoot)
|
||||
- [SSF](../ssf/Ssf.md) - Ehlers Super Smoother (2-pole IIR, infinite memory)
|
||||
|
||||
Reference in New Issue
Block a user