mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 22:08:05 +00:00
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:
+145
-74
@@ -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 <= 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user