feat(oscillators): add DSO - Ehlers Deviation-Scaled Oscillator

Implement DSO (TASC Oct 2018) with SSF 2-pole filter, RMS normalization,
and Fisher Transform (±0.99 clamp). Sealed class, O(1) streaming RMS via
RingBuffer, precomputed SSF coefficients.

New files: Dso.cs, Dso.Quantower.cs, Dso.md, dso.pine,
  Dso.Tests.cs (27), Dso.Validation.Tests.cs (7), Dso.Quantower.Tests.cs (11)

Updated: Exports.cs, _bridge.py, oscillators.py, SPEC.md,
  _sidebar.md, lib/_index.md, oscillators/_index.md,
  docs/indicators.md, docs/pinescript.md

All 19,565 tests pass, 0 warnings.
This commit is contained in:
Miha Kralj
2026-03-17 11:59:04 -07:00
parent 5fc6e27d8e
commit 7db48e2418
16 changed files with 1468 additions and 0 deletions
+1
View File
@@ -652,6 +652,7 @@ packages = ["quantalib"]
| dpo | `Dpo` | A | period |
| dstoch | `Dstoch` | E (HLC) | period |
| dosc | `Dosc` | A | rsiPeriod, ema1Period, ema2Period, sigPeriod |
| dso | `Dso` | A | period |
| dymi | `Dymi` | A | basePeriod, shortPeriod, longPeriod... |
| er | `Er` | A | period |
| fisher | `Fisher` | A | period, alpha |
+1
View File
@@ -447,6 +447,7 @@ HAS_KRI = _bind("qtl_kri", [_dp, _ci, _dp, _ci])
HAS_PSL = _bind("qtl_psl", [_dp, _ci, _dp, _ci])
HAS_DECO = _bind("qtl_deco", [_dp, _ci, _dp, _ci, _ci])
HAS_DOSC = _bind("qtl_dosc", [_dp, _ci, _dp, _ci, _ci, _ci, _ci])
HAS_DSO = _bind("qtl_dso", [_dp, _ci, _dp, _ci])
HAS_DYMI = _bind("qtl_dymi", [_dp, _ci, _dp, _ci, _ci, _ci, _ci, _ci])
HAS_CRSI = _bind("qtl_crsi", [_dp, _ci, _dp, _ci, _ci, _ci])
HAS_BBB = _bind("qtl_bbb", [_dp, _ci, _dp, _ci, _cd])
+9
View File
@@ -49,6 +49,7 @@ __all__ = [
"psl",
"deco",
"dosc",
"dso",
"dymi",
"crsi",
"bbb",
@@ -547,6 +548,14 @@ def dosc(close: object, rsi_period: int = 14, ema1_period: int = 5,
return _wrap(dst, idx, f"DOSC_{rsi_period}", "oscillators", offset)
def dso(close: object, period: int = 40, offset: int = 0, **kwargs) -> object:
"""Ehlers Deviation-Scaled Oscillator."""
period = int(kwargs.get("length", period)); offset = int(offset)
src, idx = _arr(close); n = len(src); dst = _out(n)
_check(_lib.qtl_dso(_ptr(src), n, _ptr(dst), period))
return _wrap(dst, idx, f"DSO_{period}", "oscillators", offset)
def dymi(close: object, base_period: int = 14, short_period: int = 5,
long_period: int = 10, min_period: int = 3, max_period: int = 30,
offset: int = 0, **kwargs) -> object:
+10
View File
@@ -418,6 +418,16 @@ public static unsafe partial class Exports
catch { return StatusCodes.QTL_ERR_INTERNAL; }
}
// Dso: Pattern A (src, out, int period)
[UnmanagedCallersOnly(EntryPoint = "qtl_dso")]
public static int QtlDso(double* src, int n, double* dst, int period)
{
int v = Chk1(src, dst, n); if (v != 0) return v;
v = ChkPeriod(period); if (v != 0) return v;
try { Dso.Batch(Src(src, n), Dst(dst, n), period); return StatusCodes.QTL_OK; }
catch { return StatusCodes.QTL_ERR_INTERNAL; }
}
// Dymi: Pattern A (src, out, int p1..p5)
[UnmanagedCallersOnly(EntryPoint = "qtl_dymi")]
public static int QtlDymi(double* src, int n, double* dst, int p1, int p2, int p3, int p4, int p5)