fix(docs): correct .md documentation across errors, dynamics, filters, forecasts, momentum, numerics, oscillators, reversals, statistics, trends, volatility, volume

Deep review of all indicator categories verified .md headers against .cs WarmupPeriod, parameters, inputs, and outputs. Fixes include warmup corrections, parameter documentation, output type accuracy, and Pine Script alignment.
This commit is contained in:
Miha Kralj
2026-03-10 18:38:23 -07:00
parent 8906c62dcf
commit 35a6702b06
178 changed files with 2579 additions and 998 deletions
+145 -74
View File
@@ -1,7 +1,10 @@
// IFFT: Inverse FFT Spectral Low-Pass Filter
// Reconstructs a filtered price signal by summing the DC component and
// the first H harmonics of the Hanning-windowed DFT. Output overlays on price.
// More harmonics → less smoothing; fewer harmonics → smoother output.
// IFFT: Inverse Fast Fourier Transform — Spectral Low-Pass Filter
// Reconstructs a filtered price signal by performing a forward radix-2 FFT,
// zeroing frequency bins above numHarmonics, then applying an inverse FFT.
// Output overlays on price. More harmonics → less smoothing; fewer → smoother.
//
// Algorithm: Cooley, J.W. & Tukey, J.W. (1965). Forward FFT → spectral
// truncation → inverse FFT reconstruction.
using System.Buffers;
using System.Runtime.CompilerServices;
@@ -11,16 +14,18 @@ namespace QuanTAlib;
/// <summary>
/// IFFT: Inverse FFT Spectral Low-Pass Filter
/// Reconstructs a filtered price value from the DC component plus
/// the first numHarmonics frequency bins of the Hanning-windowed DFT.
/// Reconstructs a filtered price value by performing a forward radix-2 FFT,
/// zeroing bins above numHarmonics (preserving conjugate symmetry),
/// then applying an inverse FFT to reconstruct the time-domain signal.
/// </summary>
/// <remarks>
/// Key properties:
/// - Output: reconstructed price (spectral low-pass filtered), overlays on price chart
/// - windowSize must be 32, 64, or 128
/// - windowSize must be 32, 64, or 128 (power of 2 for radix-2)
/// - numHarmonics clamped to [1, windowSize/2]
/// - WarmupPeriod = windowSize bars
/// - No allocation in Update (RingBuffer + precomputed Hanning weights)
/// - True O(N log N) radix-2 FFT/IFFT with bit-reversal permutation
/// - Pre-allocated work arrays for zero-allocation streaming
/// - Increasing harmonics increases detail (less smoothing)
/// </remarks>
[SkipLocalsInit]
@@ -28,9 +33,11 @@ public sealed class Ifft : AbstractBase
{
private readonly int _windowSize;
private readonly int _numHarmonics;
private readonly double _twoPiOverN;
private readonly double _invN;
private readonly double[] _hanning;
private readonly int[] _bitRev;
private readonly double[] _workRe;
private readonly double[] _workIm;
private readonly RingBuffer _buffer;
[StructLayout(LayoutKind.Auto)]
@@ -42,8 +49,8 @@ public sealed class Ifft : AbstractBase
/// <summary>
/// Initializes a new Ifft indicator.
/// </summary>
/// <param name="windowSize">DFT window size in bars. Must be 32, 64, or 128. Default 64.</param>
/// <param name="numHarmonics">Number of harmonics to reconstruct. Must be >= 1. Default 5.</param>
/// <param name="windowSize">FFT window size in bars. Must be 32, 64, or 128. Default 64.</param>
/// <param name="numHarmonics">Number of harmonics to preserve. Must be >= 1. Default 5.</param>
public Ifft(int windowSize = 64, int numHarmonics = 5)
{
if (windowSize != 32 && windowSize != 64 && windowSize != 128)
@@ -58,16 +65,28 @@ public sealed class Ifft : AbstractBase
_windowSize = windowSize;
_numHarmonics = Math.Min(numHarmonics, windowSize / 2);
_twoPiOverN = 2.0 * Math.PI / windowSize;
int log2N = Log2(windowSize);
_invN = 1.0 / windowSize;
// Precompute Hanning window: w[n] = 0.5 - 0.5*cos(2π*n/N), n=0..N-1
// Precompute Hanning window: w[n] = 0.5 - 0.5*cos(2π*n/N)
double twoPiOverN = 2.0 * Math.PI / windowSize;
_hanning = new double[windowSize];
for (int n = 0; n < windowSize; n++)
{
_hanning[n] = 0.5 - 0.5 * Math.Cos(_twoPiOverN * n);
_hanning[n] = 0.5 - 0.5 * Math.Cos(twoPiOverN * n);
}
// Precompute bit-reversal permutation table
_bitRev = new int[windowSize];
for (int i = 0; i < windowSize; i++)
{
_bitRev[i] = BitReverse(i, log2N);
}
// Pre-allocate work arrays (zero allocation in hot path)
_workRe = new double[windowSize];
_workIm = new double[windowSize];
_buffer = new RingBuffer(windowSize);
Name = $"Ifft({windowSize},{numHarmonics})";
WarmupPeriod = windowSize;
@@ -78,9 +97,6 @@ public sealed class Ifft : AbstractBase
/// <summary>
/// Initializes a new Ifft indicator with source for event-based chaining.
/// </summary>
/// <param name="source">Source indicator for chaining</param>
/// <param name="windowSize">DFT window size. Must be 32, 64, or 128. Default 64.</param>
/// <param name="numHarmonics">Number of harmonics to reconstruct. Must be >= 1. Default 5.</param>
public Ifft(ITValuePublisher source, int windowSize = 64, int numHarmonics = 5)
: this(windowSize, numHarmonics)
{
@@ -90,42 +106,99 @@ public sealed class Ifft : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void HandleUpdate(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int Log2(int n)
{
int p = 0;
int x = n;
while (x > 1)
{
x >>= 1;
p++;
}
return p;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int BitReverse(int x, int bits)
{
int r = 0;
for (int i = 0; i < bits; i++)
{
r = (r << 1) | (x & 1);
x >>= 1;
}
return r;
}
/// <summary>
/// Applies spectral truncation: zeroes frequency bins outside the
/// preserved range [0..numHarmonics] and their conjugate mirrors
/// [N-numHarmonics..N-1], ensuring real-valued IFFT output.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void SpectralTruncate(double[] re, double[] im, int n, int numHarmonics)
{
// Keep bins 0..numHarmonics and N-numHarmonics..N-1 (conjugate symmetry)
// Zero everything in between: bins numHarmonics+1..N-numHarmonics-1
int startZero = numHarmonics + 1;
int endZero = n - numHarmonics; // exclusive
for (int k = startZero; k < endZero; k++)
{
re[k] = 0.0;
im[k] = 0.0;
}
}
/// <summary>
/// Computes inverse FFT in-place using the conjugate method:
/// IFFT(X) = (1/N) * conj(FFT(conj(X)))
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void IfftInPlace(double[] re, double[] im, int n, int[] bitRev, double invN)
{
// Conjugate input
for (int i = 0; i < n; i++)
{
im[i] = -im[i];
}
// Forward FFT
Fft.FftInPlace(re, im, n, bitRev);
// Conjugate output and scale by 1/N
for (int i = 0; i < n; i++)
{
re[i] *= invN;
im[i] = -im[i] * invN;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double ComputeIfft()
{
var span = _buffer.GetSpan();
int n = _windowSize;
// DC component (k=0): sum of windowed values / N
double dcRe = 0.0;
for (int idx = 0; idx < n; idx++)
// Fill work arrays: windowed data (oldest→newest), imag=0
for (int i = 0; i < n; i++)
{
// span[0]=oldest, span[n-1]=newest
// dftN=0→newest, dftN=n-1→oldest → span index = n-1-dftN
double val = span[n - 1 - idx];
dcRe = Math.FusedMultiplyAdd(val, _hanning[idx], dcRe);
_workRe[i] = span[i] * _hanning[i];
_workIm[i] = 0.0;
}
double result = dcRe * _invN;
// Forward FFT
Fft.FftInPlace(_workRe, _workIm, n, _bitRev);
// Harmonics k=1..H: add 2*re/N at time n=0 (reconstruction at current bar)
for (int k = 1; k <= _numHarmonics; k++)
{
double omegaK = _twoPiOverN * k;
double re = 0.0;
// Spectral truncation: zero bins above numHarmonics
SpectralTruncate(_workRe, _workIm, n, _numHarmonics);
for (int idx = 0; idx < n; idx++)
{
double val = span[n - 1 - idx];
double xw = val * _hanning[idx];
double angle = omegaK * idx;
re = Math.FusedMultiplyAdd(xw, Math.Cos(angle), re);
}
// Inverse FFT to reconstruct filtered time-domain signal
IfftInPlace(_workRe, _workIm, n, _bitRev, _invN);
result = Math.FusedMultiplyAdd(2.0 * _invN, re, result);
}
return result;
// Return the newest sample (last position in the array)
return _workRe[n - 1];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -200,8 +273,8 @@ public sealed class Ifft : AbstractBase
}
/// <summary>
/// Computes IFFT reconstruction over a span of values using a sliding Hanning-windowed DFT.
/// Uses stackalloc for Hanning weights when windowSize &lt;= 64, otherwise ArrayPool.
/// Computes IFFT reconstruction over a span using sliding Hanning-windowed
/// radix-2 FFT → spectral truncation → inverse FFT.
/// </summary>
public static void Batch(
ReadOnlySpan<double> src, Span<double> output,
@@ -228,12 +301,14 @@ public sealed class Ifft : AbstractBase
}
int clampedHarmonics = Math.Min(numHarmonics, windowSize / 2);
int log2N = Log2(windowSize);
double twoPiOverN = 2.0 * Math.PI / windowSize;
double invN = 1.0 / windowSize;
double lastValid = 0.0;
// Precompute Hanning window
const int StackallocThreshold = 64;
double[]? rentedW = null;
double[]? rentedH = null;
scoped Span<double> hanning;
if (windowSize <= StackallocThreshold)
@@ -242,15 +317,21 @@ public sealed class Ifft : AbstractBase
}
else
{
rentedW = ArrayPool<double>.Shared.Rent(windowSize);
hanning = rentedW.AsSpan(0, windowSize);
rentedH = ArrayPool<double>.Shared.Rent(windowSize);
hanning = rentedH.AsSpan(0, windowSize);
}
// FFT work arrays and bit-reversal table
double[] workRe = ArrayPool<double>.Shared.Rent(windowSize);
double[] workIm = ArrayPool<double>.Shared.Rent(windowSize);
int[] bitRev = ArrayPool<int>.Shared.Rent(windowSize);
try
{
for (int n = 0; n < windowSize; n++)
{
hanning[n] = 0.5 - 0.5 * Math.Cos(twoPiOverN * n);
bitRev[n] = BitReverse(n, log2N);
}
for (int i = 0; i < src.Length; i++)
@@ -268,52 +349,42 @@ public sealed class Ifft : AbstractBase
continue;
}
// DC component
double dcRe = 0.0;
for (int dftN = 0; dftN < windowSize; dftN++)
// Fill work arrays with windowed data (oldest→newest)
for (int n = 0; n < windowSize; n++)
{
double v = src[i - dftN];
double v = src[i - windowSize + 1 + n];
if (!double.IsFinite(v))
{
v = lastValid;
}
dcRe = Math.FusedMultiplyAdd(v, hanning[dftN], dcRe);
workRe[n] = v * hanning[n];
workIm[n] = 0.0;
}
double result = dcRe * invN;
// Forward FFT
Fft.FftInPlace(workRe, workIm, windowSize, bitRev);
// Harmonics
for (int k = 1; k <= clampedHarmonics; k++)
{
double omegaK = twoPiOverN * k;
double re = 0.0;
// Spectral truncation
SpectralTruncate(workRe, workIm, windowSize, clampedHarmonics);
for (int dftN = 0; dftN < windowSize; dftN++)
{
double v = src[i - dftN];
if (!double.IsFinite(v))
{
v = lastValid;
}
double xw = v * hanning[dftN];
re = Math.FusedMultiplyAdd(xw, Math.Cos(omegaK * dftN), re);
}
result = Math.FusedMultiplyAdd(2.0 * invN, re, result);
}
// Inverse FFT
IfftInPlace(workRe, workIm, windowSize, bitRev, invN);
// Extract newest sample
double result = workRe[windowSize - 1];
lastValid = result;
output[i] = result;
}
}
finally
{
if (rentedW != null)
if (rentedH != null)
{
ArrayPool<double>.Shared.Return(rentedW);
ArrayPool<double>.Shared.Return(rentedH);
}
ArrayPool<double>.Shared.Return(workRe);
ArrayPool<double>.Shared.Return(workIm);
ArrayPool<int>.Shared.Return(bitRev);
}
}
+69 -60
View File
@@ -1,4 +1,4 @@
# IFFT: Inverse Fast Fourier Transform (Spectral Filter)
# IFFT: Inverse Fast Fourier Transform (Spectral Low-Pass Filter)
| Property | Value |
| ---------------- | -------------------------------- |
@@ -6,64 +6,68 @@
| **Inputs** | Source (close) |
| **Parameters** | `windowSize` (default 64), `numHarmonics` (default 5) |
| **Outputs** | Single series (Ifft) |
| **Output range** | Varies (see docs) |
| **Warmup** | 1 bar |
| **Output range** | Varies (overlays on price) |
| **Warmup** | windowSize bars |
### TL;DR
- The Inverse FFT indicator reconstructs a smoothed version of the price series by performing a forward DFT, retaining only the lowest-frequency harm...
- Parameterized by `windowsize` (default 64), `numharmonics` (default 5).
- Output range: Varies (see docs).
- Requires 1 bar of warmup before first valid output (IsHot = true).
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
- The IFFT indicator reconstructs a smoothed version of the price series using a true forward FFT → spectral truncation → inverse FFT pipeline.
- Parameterized by `windowSize` (default 64), `numHarmonics` (default 5).
- Output range: Varies (overlays on price chart).
- Requires windowSize bars of warmup before first valid output (IsHot = true).
- True $O(N \log N)$ radix-2 FFT/IFFT with bit-reversal permutation and Cooley-Tukey butterflies.
The Inverse FFT indicator reconstructs a smoothed version of the price series by performing a forward DFT, retaining only the lowest-frequency harmonics, and synthesizing the output via inverse transform. The result is a spectral low-pass filter that preserves the dominant cyclical components while discarding high-frequency noise. By controlling the number of retained harmonics $H$, the user adjusts the smoothness/responsiveness trade-off: $H = 1$ yields a near-sinusoidal trend, while $H = N/2$ reproduces the original (windowed) signal. The indicator overlays on price and provides a frequency-domain alternative to conventional moving averages.
The IFFT indicator reconstructs a smoothed version of the price series by performing a true radix-2 forward FFT, zeroing frequency bins above the specified number of harmonics (spectral truncation), then applying a true inverse FFT to reconstruct the filtered time-domain signal. The result is a spectral low-pass filter that preserves the dominant cyclical components while discarding high-frequency noise. By controlling the number of retained harmonics $H$, the user adjusts the smoothness/responsiveness trade-off: $H = 1$ yields a near-sinusoidal trend, while $H = N/2$ reproduces the original (windowed) signal. The indicator overlays on price and provides a frequency-domain alternative to conventional moving averages.
## Historical Context
Spectral filtering via Fourier decomposition dates to Joseph Fourier's 1822 work on heat conduction, where he showed that any periodic function can be represented as a sum of sinusoids. The idea of reconstructing a signal from a subset of its Fourier coefficients is foundational to signal compression (JPEG, MP3) and has been applied to financial time series since the 1970s.
Spectral filtering via Fourier decomposition dates to Joseph Fourier's 1822 work on heat conduction, where he showed that any periodic function can be represented as a sum of sinusoids. The Cooley-Tukey FFT algorithm (1965) made real-time spectral analysis practical by reducing complexity from $O(N^2)$ to $O(N \log N)$.
John Ehlers brought spectral methods to mainstream technical analysis through his books on cycle analytics. His approach typically uses the DFT to identify the dominant cycle, then constructs adaptive filters tuned to that cycle. The IFFT indicator takes the complementary approach: rather than extracting a single cycle period, it reconstructs the signal from the $H$ lowest-frequency components, producing a multi-harmonic trend estimate.
John Ehlers brought spectral methods to mainstream technical analysis through his books on cycle analytics. The IFFT indicator implements the classic spectral filtering paradigm: forward FFT to decompose into frequency components, selective retention of low-frequency bins, and inverse FFT to reconstruct the filtered signal.
The Hanning window applied before the forward DFT reduces spectral leakage, ensuring that the retained harmonics accurately represent the true low-frequency content rather than artifacts of the window boundary. The inverse step only uses the real part of the synthesis (cosine terms), since the output must be a real-valued price estimate. The factor of 2 in the inverse accounts for the conjugate symmetry of real-valued DFT coefficients.
The Hanning window applied before the forward FFT reduces spectral leakage, ensuring that the retained harmonics accurately represent the true low-frequency content rather than artifacts of the window boundary. Conjugate symmetry is preserved during spectral truncation to guarantee real-valued reconstruction.
## Architecture and Physics
The computation has three stages executed per bar:
The computation has four stages executed per bar:
**Stage 1: DC component** computes the windowed mean of the source over the window. This is the zero-frequency (average level) component:
**Stage 1: Forward FFT** applies a Hanning window to the rolling price buffer, then performs an in-place radix-2 Cooley-Tukey FFT with bit-reversal permutation:
$$\text{DC} = \frac{1}{N}\sum_{n=0}^{N-1} x[n] \cdot w[n]$$
$$X[k] = \text{FFT}\!\left(x[n] \cdot w[n]\right)$$
**Stage 2: Forward DFT for harmonics $k = 1$ to $H$** computes the real and imaginary Fourier coefficients for each retained harmonic. The Hanning window $w[n] = 0.5 - 0.5\cos(2\pi n/N)$ is applied to every sample.
where $w[n] = 0.5 - 0.5\cos(2\pi n/N)$.
**Stage 3: Inverse synthesis** reconstructs the current bar's value by summing the DC component plus twice the real part of each harmonic evaluated at $n = 0$ (the current bar):
**Stage 2: Spectral truncation** zeroes frequency bins outside the preserved range, keeping bins $k = 0, 1, \ldots, H$ and their conjugate mirrors $k = N-H, \ldots, N-1$:
$$\hat{x}[0] = \frac{\text{DC}_{\text{Re}}}{N} + \sum_{k=1}^{H} \frac{2 \cdot \text{Re}(X[k])}{N}$$
$$\tilde{X}[k] = \begin{cases} X[k] & \text{if } k \le H \text{ or } k \ge N-H \\ 0 & \text{otherwise} \end{cases}$$
The factor $2/N$ accounts for: (1) the $1/N$ normalization of the inverse DFT, and (2) the factor of 2 from collapsing the conjugate-symmetric negative frequencies.
This preserves conjugate symmetry ($\tilde{X}[N-k] = \tilde{X}[k]^*$), ensuring the inverse FFT produces real-valued output.
**Complexity**: The forward DFT for $H$ harmonics costs $O(N \cdot H)$ multiply-adds per bar. With $N = 64$ and $H = 5$ (defaults), this is ~320 multiply-adds per bar. The inverse synthesis at $n = 0$ reduces to just summing the real components, costing $O(H)$.
**Stage 3: Inverse FFT** reconstructs the filtered time-domain signal using the conjugate method:
**Smoothness control**: Fewer harmonics produce smoother output but introduce more lag and lose detail. The relationship between harmonics and equivalent moving average length is roughly: $H$ harmonics approximate the smoothness of an $N/(2H)$-period moving average, but with better frequency selectivity (sharper cutoff).
$$\hat{x}[n] = \frac{1}{N} \cdot \overline{\text{FFT}\!\left(\overline{\tilde{X}[k]}\right)}$$
This reuses the forward FFT algorithm by conjugating inputs, applying FFT, conjugating outputs, and scaling by $1/N$.
**Stage 4: Sample extraction** returns the value at position $N-1$ (the newest bar in the window).
**Complexity**: Two FFT passes of $O(N \log N)$ each, plus $O(N)$ for windowing and spectral truncation. Total: $O(N \log N)$ per bar.
**Smoothness control**: Fewer harmonics produce smoother output but introduce more lag. The relationship between harmonics and equivalent moving average length is roughly: $H$ harmonics approximate the smoothness of an $N/(2H)$-period moving average, with better frequency selectivity (sharper cutoff).
## Mathematical Foundation
The **forward DFT** with Hanning window:
The **forward FFT** with Hanning window (radix-2 Cooley-Tukey):
$$X[k] = \sum_{n=0}^{N-1} x[n] \cdot w[n] \cdot e^{-j 2\pi k n / N}$$
$$X[k] = \text{FFT}_N\!\left(x[n] \cdot w[n]\right), \quad k = 0, 1, \ldots, N-1$$
where $w[n] = 0.5 - 0.5\cos(2\pi n / N)$.
**Spectral truncation** (ideal low-pass in frequency domain):
The **inverse DFT** evaluated at the current bar ($n = 0$):
$$\tilde{X}[k] = X[k] \cdot H_{\text{LP}}[k], \quad H_{\text{LP}}[k] = \begin{cases} 1 & k \le H \text{ or } k \ge N-H \\ 0 & \text{otherwise} \end{cases}$$
$$\hat{x}[0] = \frac{1}{N}\sum_{k=0}^{N-1} X[k] \cdot e^{j 2\pi k \cdot 0 / N} = \frac{1}{N}\sum_{k=0}^{N-1} X[k]$$
**Inverse FFT** via conjugation:
Since $e^{j \cdot 0} = 1$, the inverse at $n = 0$ is simply the sum of all retained coefficients divided by $N$.
For a real-valued signal, $X[N-k] = X[k]^*$, so:
$$\hat{x}[0] = \frac{X[0]}{N} + \frac{2}{N}\sum_{k=1}^{H} \text{Re}(X[k])$$
$$\hat{x}[n] = \frac{1}{N} \cdot \overline{\text{FFT}_N\!\left(\overline{\tilde{X}[k]}\right)}$$
**Parseval's theorem** relates the energy retained:
@@ -75,27 +79,27 @@ $$\frac{\sum_{k=0}^{H} |X[k]|^2}{\sum_{k=0}^{N/2} |X[k]|^2} = \text{fraction of
IFFT(source, windowSize, numHarmonics):
N = windowSize
H = min(numHarmonics, N/2)
twoPiOverN = 2 * pi / N
// DC component (k=0)
dcRe = 0
// Stage 1: Window + Forward FFT
for n = 0 to N-1:
w = 0.5 - 0.5 * cos(twoPiOverN * n)
dcRe += source[n] * w
result = dcRe / N
workRe[n] = source[n] * hanning[n]
workIm[n] = 0
FFT_InPlace(workRe, workIm, N)
// Harmonics k=1..H
for k = 1 to H:
re = 0; im = 0
for n = 0 to N-1:
w = 0.5 - 0.5 * cos(twoPiOverN * n)
xw = source[n] * w
angle = twoPiOverN * k * n
re += xw * cos(angle)
im -= xw * sin(angle)
result += 2 * re / N // inverse at n=0
// Stage 2: Spectral truncation
for k = H+1 to N-H-1:
workRe[k] = 0
workIm[k] = 0
return result
// Stage 3: Inverse FFT (via conjugation)
for i = 0 to N-1: workIm[i] = -workIm[i]
FFT_InPlace(workRe, workIm, N)
for i = 0 to N-1:
workRe[i] /= N
workIm[i] = -workIm[i] / N
// Stage 4: Extract newest sample
return workRe[N-1]
```
@@ -103,32 +107,37 @@ IFFT(source, windowSize, numHarmonics):
### Operation Count (Streaming Mode)
IFFT (Inverse DFT reconstruction) sums B frequency components back into the time domain — O(N*B) per bar.
IFFT performs two radix-2 FFT passes (forward + inverse) plus spectral truncation — O(N log N) per bar.
| Operation | Count | Cost (cycles) | Subtotal |
| :--- | :---: | :---: | :---: |
| Complex multiply-accumulate (N * B) | N*B | 4 cy | ~4*N*B cy |
| cos/sin table lookup (precomputed) | 2*N*B | 0 cy | ~0 cy |
| Division by N for normalization | N | 1 cy | ~N cy |
| NaN guard + state update | 1 | 2 cy | ~2 cy |
| **Total (N=64, B=10)** | **O(N*B)** | — | **~2626 cy** |
| Hanning window multiply | N | 2 cy | ~2N cy |
| Forward FFT (N/2 × log₂N butterflies) | N/2 × log₂N | 8 cy | ~4N·log₂N cy |
| Spectral truncation | N-2H | 1 cy | ~(N-2H) cy |
| Conjugation (2×) | 2N | 1 cy | ~2N cy |
| Inverse FFT (N/2 × log₂N butterflies) | N/2 × log₂N | 8 cy | ~4N·log₂N cy |
| Scale by 1/N | N | 1 cy | ~N cy |
| **Total (N=64, H=5)** | **O(N log N)** | — | **~3254 cy** |
Same complexity as forward FFT. Precomputed trig tables allow the inner loop to reduce to 4 FMAs per bin. Paired with FFT for frequency-domain filtering.
Two FFT passes dominate cost. Pre-allocated work arrays ensure zero allocation in the hot path.
### Batch Mode (SIMD Analysis)
| Operation | Vectorizable? | Notes |
| :--- | :---: | :--- |
| Complex MAC (re*cos - im*sin) | Yes | FMA with precomputed table |
| Normalization | Yes | Vector divide by N |
| Output time-domain signal | Yes | Full SIMD reconstruction |
| Hanning window application | Yes | Vector multiply with precomputed weights |
| FFT butterfly operations | Yes | Complex FMA on paired elements |
| Spectral truncation (zeroing) | Yes | Vector zero-fill |
| IFFT butterfly operations | Yes | Same as forward FFT |
| Scale by 1/N | Yes | Vector multiply by constant |
Same SIMD profile as FFT forward pass. 3-4× batch speedup expected over scalar using Vector<double> FMA.
Good SIMD potential: both FFT passes are vectorizable. Expected 2× speedup over scalar for N=64.
## Resources
- Cooley, J.W. & Tukey, J.W. "An Algorithm for the Machine Calculation of Complex Fourier Series." *Mathematics of Computation*, 1965.
- Fourier, J.B.J. "Theorie Analytique de la Chaleur." Firmin Didot, 1822.
- Ehlers, J.F. "Cycle Analytics for Traders." Wiley, 2013.
- Oppenheim, A.V. & Schafer, R.W. "Discrete-Time Signal Processing." 3rd edition, Pearson, 2010.
- Bloomfield, P. "Fourier Analysis of Time Series: An Introduction." 2nd edition, Wiley, 2000.
- Priestley, M.B. "Spectral Analysis and Time Series." Academic Press, 1981.
- PineScript reference: [`ifft.pine`](ifft.pine)