feat: add LPF - Ehlers Linear Predictive Filter (TASC Jan 2025)

Implements Ehlers' Linear Predictive Filter for dominant cycle detection:
- Roofing filter (HP + SuperSmoother) → AGC → Griffiths adaptive predictor
- DFT spectrum from predictor coefficients → Center of Gravity dominant cycle
- Outputs: DominantCycle, Signal (AGC-normalized), Predict (one-bar-ahead)

Files added:
- lib/cycles/lpf/Lpf.cs (core implementation, sealed class)
- lib/cycles/lpf/Lpf.Quantower.cs (3 LineSeries: Cycle, Signal, Predict)
- lib/cycles/lpf/Lpf.md (canonical template v3 documentation)
- lib/cycles/lpf/lpf.pine (PineScript v6 reference)
- lib/cycles/lpf/tests/Lpf.Tests.cs (38 unit tests)
- lib/cycles/lpf/tests/Lpf.Quantower.Tests.cs (22 adapter tests)

Updated: index files, Python bridge (Exports.cs, _bridge.py, cycles.py)
This commit is contained in:
Miha Kralj
2026-03-17 20:24:54 -07:00
parent 547367790b
commit 6ac30d37e6
14 changed files with 1607 additions and 2 deletions
+1
View File
@@ -573,6 +573,7 @@ HAS_DSP = _bind("qtl_dsp", [_dp, _ci, _dp, _ci])
HAS_CCOR = _bind("qtl_ccor", [_dp, _ci, _dp, _ci, _cd])
HAS_EBSW = _bind("qtl_ebsw", [_dp, _ci, _dp, _ci, _ci])
HAS_ACP = _bind("qtl_acp", [_dp, _ci, _dp, _ci, _ci, _ci, _ci])
HAS_LPF = _bind("qtl_lpf", [_dp, _ci, _dp, _ci, _ci, _ci])
HAS_AMFM = _bind("qtl_amfm", [_dp, _dp, _ci, _dp, _dp, _ci])
# ── Numerics (Exports.cs — manual) ──
+11
View File
@@ -13,6 +13,7 @@ __all__ = [
"ht_dcphase",
"ht_phasor",
"ht_sine",
"lpf",
"lunar",
"solar",
"ssfdsp",
@@ -79,6 +80,16 @@ def ht_sine(close: object, offset: int = 0, **kwargs) -> object:
return _wrap_multi({"sine": sine, "leadSine": leadSine}, idx, "cycles", offset)
def lpf(close: object, lower_bound: int = 18, upper_bound: int = 40,
data_length: int = 40, offset: int = 0, **kwargs) -> object:
"""Ehlers Linear Predictive Filter (dominant cycle)."""
lower_bound = int(lower_bound); upper_bound = int(upper_bound)
data_length = int(data_length); offset = int(offset)
src, idx = _arr(close); n = len(src); dst = _out(n)
_check(_lib.qtl_lpf(_ptr(src), n, _ptr(dst), lower_bound, upper_bound, data_length))
return _wrap(dst, idx, f"LPF_{lower_bound}_{upper_bound}", "cycles", offset)
def lunar(close: object, offset: int = 0, **kwargs) -> object:
"""Lunar Cycle."""
offset = int(offset)
+10
View File
@@ -1546,6 +1546,16 @@ public static unsafe partial class Exports
catch { return StatusCodes.QTL_ERR_INTERNAL; }
}
// Lpf: Pattern A (source → output, int lowerBound, int upperBound, int dataLength)
[UnmanagedCallersOnly(EntryPoint = "qtl_lpf")]
public static int QtlLpf(double* src, int n, double* dst, int lowerBound, int upperBound, int dataLength)
{
int v = Chk1(src, dst, n); if (v != 0) return v;
v = ChkPeriod(lowerBound); if (v != 0) return v;
try { Lpf.Batch(Src(src, n), Dst(dst, n), lowerBound, upperBound, dataLength); return StatusCodes.QTL_OK; }
catch { return StatusCodes.QTL_ERR_INTERNAL; }
}
// ═══════════════════════════════════════════════════════════════════════
// §8.14 Numerics / transforms
// ═══════════════════════════════════════════════════════════════════════