feat(dynamics): add PlusDI, MinusDI, PlusDM, MinusDM indicators

Complete thin Dx-composition wrapper indicators with full test coverage:

- PlusDi/MinusDi: Directional Indicator wrappers (DiPlus/DiMinus from Dx)
- PlusDm/MinusDm: Directional Movement wrappers (DmPlus/DmMinus from Dx)
- Individual validation tests per indicator directory (TALib, Skender, bounds)
- Combined unit tests (DiDm.Tests.cs) and validation tests (DiDm.Validation.Tests.cs)
- Quantower wrappers + tests for all 4 indicators
- PineScript v6 implementations with compensated RMA
- Normalized .md documentation for all indicators and categories
- 182 tests passing, 0 failures
This commit is contained in:
Miha Kralj
2026-03-11 20:21:52 -07:00
parent 56b86bebfb
commit 33d20f2a18
437 changed files with 4589 additions and 2792 deletions
-2
View File
@@ -1,7 +1,5 @@
# Cycles
> "The market is a discounting mechanism that anticipates cycles before they complete." Unknown
Cycle analysis identifies repeating patterns in price data. John Ehlers pioneered digital signal processing techniques for financial cycles, using Hilbert transforms and autocorrelation to detect dominant periods. Cycles exist but are non-stationary: period and amplitude shift over time.
## Indicators
+2 -42
View File
@@ -1,5 +1,7 @@
# CCOR: Ehlers Correlation Cycle
> *Correlation cycles reveal hidden periodicities by measuring how well price correlates with a rotating reference wave.*
| Property | Value |
| ---------------- | -------------------------------- |
| **Category** | Cycle |
@@ -94,48 +96,6 @@ Where:
- Real: $y_k = \cos(2\pi k / N)$
- Imaginary: $y_k = -\sin(2\pi k / N)$
### Pseudo-code
```
function CCOR(source, period, threshold):
// Real correlation
Sx_r = Sy_r = Sxx_r = Sxy_r = Syy_r = 0
for k = 0 to period-1:
x = source[k]
y = cos(2π * k / period)
Sx_r += x; Sy_r += y
Sxx_r += x*x; Sxy_r += x*y; Syy_r += y*y
denom_r = (N*Sxx_r - Sx_r²) * (N*Syy_r - Sy_r²)
real = denom_r > 0 ? (N*Sxy_r - Sx_r*Sy_r) / √denom_r : 0
// Imaginary correlation (same accumulators for x, different y)
Sx_i = Sy_i = Sxx_i = Sxy_i = Syy_i = 0
for k = 0 to period-1:
x = source[k]
y = -sin(2π * k / period)
Sx_i += x; Sy_i += y
Sxx_i += x*x; Sxy_i += x*y; Syy_i += y*y
denom_i = (N*Sxx_i - Sx_i²) * (N*Syy_i - Sy_i²)
imag = denom_i > 0 ? (N*Sxy_i - Sx_i*Sy_i) / √denom_i : 0
// Phasor angle
angle = 0
if imag ≠ 0: angle = 90 + atan(real/imag) * (180/π)
if imag > 0: angle -= 180
// Monotonic constraint
angle = max(angle, prev_angle)
prev_angle = angle
// State detection
Δθ = |angle - saved_prev_angle|
state = 0
if Δθ < threshold and angle ≥ 0: state = +1
if Δθ < threshold and angle ≤ 0: state = -1
return [real, imag, angle, state]
```
### Output Interpretation
| Output | Range | Meaning |
+2 -27
View File
@@ -1,5 +1,7 @@
# CCYC: Ehlers Cyber Cycle
> *The Cyber Cycle isolator extracts the dominant cycle component while suppressing trend — pure periodicity distilled.*
| Property | Value |
| ---------------- | -------------------------------- |
| **Category** | Cycle |
@@ -109,33 +111,6 @@ $$z^2 - 2(1-\alpha)z + (1-\alpha)^2 = 0$$
has a double pole at $z = 1 - \alpha$. For $\alpha = 0.07$, the pole is at $z = 0.93$, well inside the unit circle (stable), with a $-3$ dB cutoff period of approximately $\frac{2\pi}{\alpha} \approx 90$ bars.
### Pseudo-code
```
function CCYC(source, alpha):
validate: 0 < alpha < 1
// FIR smoother
smooth = (source[0] + 2*source[1] + 2*source[2] + source[3]) / 6
// IIR coefficients
c_hp = (1 - 0.5*alpha)²
c_fb1 = 2*(1 - alpha)
c_fb2 = -(1 - alpha)²
if bar_count < 7:
// Bootstrap: second-difference of raw price
cycle = (source[0] - 2*source[1] + source[2]) / 4
else:
// Steady-state: 2-pole high-pass on smoothed input
cycle = c_hp * (smooth - 2*smooth[1] + smooth[2])
+ c_fb1 * cycle[1] + c_fb2 * cycle[2]
trigger = cycle[1]
return [cycle, trigger]
```
### Output Interpretation
| Output | Description |
+2 -25
View File
@@ -1,5 +1,7 @@
# CG: Ehlers Center of Gravity
> *Center of Gravity locates the balance point of price over a window, anticipating turns before they arrive.*
| Property | Value |
| ---------------- | -------------------------------- |
| **Category** | Cycle |
@@ -54,31 +56,6 @@ Streaming uses running sums for both numerator and denominator: $O(1)$ per bar w
|-----------|-------------|---------|------------|
| `period` | Lookback window length | 10 | $> 0$ |
### Pseudo-code
```
function CG(source, period):
buffer ← RingBuffer(period)
runNum ← 0 // weighted sum
runDen ← 0 // simple sum
for each price in source:
buffer.Add(price)
if buffer.Count < period: continue
// Compute from buffer (or maintain running sums)
num = 0
den = 0
for i = 0 to period-1:
w = i + 1
num += w * buffer[i]
den += buffer[i]
cg = (den ≠ 0) ? (num / den) - (period + 1) / 2.0 : 0
emit cg
```
### Output Interpretation
| Condition | Meaning |
+2 -28
View File
@@ -1,5 +1,7 @@
# DSP: Ehlers Detrended Synthetic Price
> *Detrended synthetic price removes the trend to expose the oscillation underneath — the signal beneath the drift.*
| Property | Value |
| ---------------- | -------------------------------- |
| **Category** | Cycle |
@@ -66,34 +68,6 @@ $O(1)$ per bar with $O(1)$ memory. Two EMA state variables plus two bias correct
|-----------|-------------|---------|------------|
| `period` | Dominant cycle period | 40 | $\geq 4$ |
### Pseudo-code
```
function DSP(source, period):
pFast ← max(2, round(period / 4))
pSlow ← max(3, round(period / 2))
αFast ← 2 / (pFast + 1)
αSlow ← 2 / (pSlow + 1)
emaFastRaw ← 0
emaSlowRaw ← 0
decayFast ← 1.0 // (1 - αFast)^n
decaySlow ← 1.0 // (1 - αSlow)^n
for each price in source:
emaFastRaw ← FMA(αFast, price, (1 - αFast) * emaFastRaw)
emaSlowRaw ← FMA(αSlow, price, (1 - αSlow) * emaSlowRaw)
decayFast *= (1 - αFast)
decaySlow *= (1 - αSlow)
emaFast ← emaFastRaw / (1 - decayFast)
emaSlow ← emaSlowRaw / (1 - decaySlow)
dsp ← emaFast - emaSlow
emit dsp
```
### Output Interpretation
| Condition | Meaning |
+2 -45
View File
@@ -1,5 +1,7 @@
# EACP: Ehlers Autocorrelation Periodogram
> *Autocorrelation periodogram scans every possible cycle length and ranks them by strength — a spectral fingerprint of the market.*
| Property | Value |
| ---------------- | -------------------------------- |
| **Category** | Cycle |
@@ -72,51 +74,6 @@ $O(N \times M)$ per bar where $N$ is the period range and $M$ is the averaging l
| `maxPeriod` | Maximum period to evaluate | 48 | $> minPeriod$ |
| `enhance` | Apply cubic emphasis to spectral peaks | true | |
### Pseudo-code
```
function EACP(source, minPeriod, maxPeriod, enhance):
N ← maxPeriod - minPeriod + 1
M ← maxPeriod // averaging window
hpBuf ← HighPassFilter(source)
ssfBuf ← SuperSmoother(hpBuf)
power[N] ← {0}
smoothPower[N] ← {0}
for each bar:
// Autocorrelation for each lag
corr[0..maxPeriod] ← PearsonAutocorrelation(ssfBuf, M)
// DFT: convert autocorrelation to power spectrum
for p = minPeriod to maxPeriod:
cosPower ← 0
for k = 0 to M-1:
cosPower += corr[k] * cos(2π * k / p)
power[p] ← cosPower²
// Exponential smoothing of spectrum
for p = minPeriod to maxPeriod:
smoothPower[p] ← 0.2 * power[p] + 0.8 * smoothPower[p]
// Optional cubic enhancement
if enhance:
for p: smoothPower[p] ← smoothPower[p]³
// AGC normalization
maxPow ← max(smoothPower)
for p: smoothPower[p] /= maxPow // normalize to [0, 1]
// Center-of-gravity dominant cycle
num ← 0; den ← 0
for p = minPeriod to maxPeriod:
num += smoothPower[p] * p
den += smoothPower[p]
dominantCycle ← (den > 0) ? num / den : (minPeriod + maxPeriod) / 2
emit dominantCycle
```
### Output Interpretation
| Output | Meaning |
+2 -39
View File
@@ -1,5 +1,7 @@
# EBSW: Ehlers Even Better Sinewave
> *Even Better Sinewave refines cycle detection by combining bandpass filtering with adaptive gain normalization.*
| Property | Value |
| ---------------- | -------------------------------- |
| **Category** | Cycle |
@@ -78,45 +80,6 @@ b = 2·a·cos(√2·π / ssfLength)
c₁ = (1 - b + a²) / 2
```
### Pseudo-code
```
function EBSW(source, hpLength, ssfLength):
// Precompute HP coefficient
α₁ ← (1 - sin(2π/hpLength)) / cos(2π/hpLength)
// Precompute SSF coefficients
a ← exp(-√2·π / ssfLength)
b ← 2·a·cos(√2·π / ssfLength)
c₁ ← (1 - b + a²) / 2
hp_prev ← 0; p_prev ← 0
filt_1 ← 0; filt_2 ← 0
for each price in source:
// High-pass filter
hp ← 0.5·(1 + α₁)·(price - p_prev) + α₁·hp_prev
// Super-smoother
filt ← c₁·(hp + hp_prev) + b·filt_1 - a²·filt_2
// Wave (3-bar average of filtered signal)
wave ← (filt + filt_1 + filt_2) / 3
// Power (3-bar RMS²)
power ← (filt² + filt_1² + filt_2²) / 3
// AGC normalization
ebsw ← (power > 0) ? wave / √power : 0
ebsw ← clamp(ebsw, -1, +1)
// Shift state
hp_prev ← hp; p_prev ← price
filt_2 ← filt_1; filt_1 ← filt
emit ebsw
```
### Output Interpretation
| Condition | Meaning |
+2 -54
View File
@@ -1,5 +1,7 @@
# HOMOD: Ehlers Homodyne Discriminator
> *The homodyne discriminator locks onto a cycle's frequency by comparing successive analytic signal rotations.*
| Property | Value |
| ---------------- | -------------------------------- |
| **Category** | Cycle |
@@ -71,60 +73,6 @@ $O(1)$ per bar with $O(1)$ memory. The pipeline consists entirely of fixed-depth
| `minPeriod` | Minimum detectable period | 6.0 | $> 0$ |
| `maxPeriod` | Maximum detectable period | 50.0 | $> minPeriod$ |
### Pseudo-code
```
function HOMOD(source, minPeriod, maxPeriod):
A ← 0.0962; B ← 0.5769
smoothBuf ← CircularBuffer(7)
detBuf ← CircularBuffer(7)
I2_prev ← 0; Q2_prev ← 0
Re_prev ← 0; Im_prev ← 0
period_prev ← (minPeriod + maxPeriod) / 2
for each price in source:
// 4-bar WMA
smooth ← (4·price + 3·p[1] + 2·p[2] + p[3]) / 10
smoothBuf.Add(smooth)
// Detrender (Hilbert FIR)
det ← A·smooth[0] + B·smooth[2] - B·smooth[4] - A·smooth[6]
detBuf.Add(det)
// I1 = det[3], Q1 = Hilbert(det)
I1 ← det[3]
Q1 ← A·det[0] + B·det[2] - B·det[4] - A·det[6]
// Hilbert of I1 and Q1
jI ← HilbertFIR(I1_history)
jQ ← HilbertFIR(Q1_history)
// Phasor components (smoothed)
I2 ← 0.2·(I1 - jQ) + 0.8·I2_prev
Q2 ← 0.2·(Q1 + jI) + 0.8·Q2_prev
// Homodyne mixing
re ← 0.2·(I2·I2_prev + Q2·Q2_prev) + 0.8·Re_prev
im ← 0.2·(I2·Q2_prev - Q2·I2_prev) + 0.8·Im_prev
// Period extraction
if im ≠ 0 and re ≠ 0:
period ← 2π / atan2(im, re)
else:
period ← period_prev
period ← clamp(period, minPeriod, maxPeriod)
period ← 0.33·period + 0.67·period_prev
// Update state
I2_prev ← I2; Q2_prev ← Q2
Re_prev ← re; Im_prev ← im
period_prev ← period
emit period
```
### Output Interpretation
| Output | Meaning |
+2 -41
View File
@@ -1,5 +1,7 @@
# HT_DCPERIOD: Ehlers Hilbert Transform Dominant Cycle Period
> *The Hilbert Transform extracts the dominant cycle period by converting price into an analytic signal and measuring its phase rate.*
| Property | Value |
| ---------------- | -------------------------------- |
| **Category** | Cycle |
@@ -68,47 +70,6 @@ $O(1)$ per bar. Fixed Hilbert cascade with circular buffers totaling approximate
The period range [6, 50] and all smoothing constants are fixed by the TA-Lib specification.
### Pseudo-code
```
function HT_DCPERIOD(source):
A ← 0.0962; B ← 0.5769
smoothBuf ← CircularBuffer(7)
detBuf, q1Buf, i1Buf ← CircularBuffers
I2 ← 0; Q2 ← 0
Re ← 0; Im ← 0
period ← 15 // initial estimate
for each price in source:
// Step 1: WMA smooth
smooth ← (4·price + 3·p[1] + 2·p[2] + p[3]) / 10
// Step 2: Hilbert FIR (adaptive to period)
adj ← A + B // coefficient adjustment
det ← adj·(smooth[0] - smooth[6]) + B·(smooth[2] - smooth[4])
Q1 ← adj·(det[0] - det[6]) + B·(det[2] - det[4])
I1 ← det[3]
jI ← adj·(I1[0] - I1[6]) + B·(I1[2] - I1[4])
jQ ← adj·(Q1[0] - Q1[6]) + B·(Q1[2] - Q1[4])
// Step 3: Phasor (EMA smoothed)
I2 ← 0.2·(I1 - jQ) + 0.8·I2
Q2 ← 0.2·(Q1 + jI) + 0.8·Q2
// Step 4: Homodyne discriminator
Re ← 0.2·(I2·I2_prev + Q2·Q2_prev) + 0.8·Re
Im ← 0.2·(I2·Q2_prev - Q2·I2_prev) + 0.8·Im
// Step 5: Period
if Im ≠ 0 and Re ≠ 0:
p ← 2π / atan(Im / Re)
p ← clamp(p, 6, 50)
period ← 0.33·p + 0.67·period
emit period
```
### Output Interpretation
| Output | Meaning |
+2 -32
View File
@@ -1,5 +1,7 @@
# HT_DCPHASE: Ehlers Hilbert Transform Dominant Cycle Phase
> *Dominant cycle phase tracks where price sits within its current cycle — the angular position of the market's heartbeat.*
| Property | Value |
| ---------------- | -------------------------------- |
| **Category** | Cycle |
@@ -64,38 +66,6 @@ $O(P)$ per bar where $P$ is the smoothed period (typically 6-50), due to the DFT
All internal constants are fixed by the TA-Lib specification.
### Pseudo-code
```
function HT_DCPHASE(source):
// Same Hilbert cascade as HT_DCPERIOD
// ... (WMA smooth, Hilbert FIR, phasor, homodyne)
// Produces: smoothPeriod, smoothPriceBuf
for each bar (after warmup):
P ← round(smoothPeriod)
// DFT accumulation over dominant period
realPart ← 0; imagPart ← 0
for i = 0 to P-1:
realPart += sin(2π·i / P) · smoothPriceBuf[t - i]
imagPart += cos(2π·i / P) · smoothPriceBuf[t - i]
// Phase extraction
if |imagPart| > 0:
dcPhase ← atan(realPart / imagPart) · (180/π)
else:
dcPhase ← 90 · sign(realPart)
if imagPart > 0: dcPhase -= 180
dcPhase += 90
// Wrap to [-45, 315]
if dcPhase < -45: dcPhase += 360
emit dcPhase
```
### Phase Quadrant Interpretation
| Phase Range | Cycle Position |
+2 -31
View File
@@ -1,5 +1,7 @@
# HT_PHASOR: Ehlers Hilbert Transform Phasor Components
> *Phasor components decompose price into in-phase and quadrature parts, mapping the cycle as a rotating vector.*
| Property | Value |
| ---------------- | -------------------------------- |
| **Category** | Cycle |
@@ -58,37 +60,6 @@ $O(1)$ per bar. Fixed Hilbert cascade with circular buffers. Warmup: 32 bars (TA
|-----------|-------------|---------|------------|
| (none) | No user-configurable parameters | | |
### Pseudo-code
```
function HT_PHASOR(source):
A ← 0.0962; B ← 0.5769
smoothBuf ← CircularBuffer(7)
detBuf, q1Buf, i1Buf ← CircularBuffers
I2 ← 0; Q2 ← 0
for each price in source:
// WMA smooth
smooth ← (4·price + 3·p[1] + 2·p[2] + p[3]) / 10
smoothBuf.Add(smooth)
// Hilbert FIR (adaptive)
det ← A·smooth[0] + B·smooth[2] - B·smooth[4] - A·smooth[6]
Q1 ← A·det[0] + B·det[2] - B·det[4] - A·det[6]
I1 ← det[3]
// Hilbert of I1 and Q1
jI ← A·I1[0] + B·I1[2] - B·I1[4] - A·I1[6]
jQ ← A·Q1[0] + B·Q1[2] - B·Q1[4] - A·Q1[6]
// Phasor components (EMA smoothed)
I2 ← 0.2·(I1 - jQ) + 0.8·I2
Q2 ← 0.2·(Q1 + jI) + 0.8·Q2
emit InPhase = I2, Quadrature = Q2
```
### Phasor Crossover Signals
| Condition | Signal |
+2 -21
View File
@@ -1,5 +1,7 @@
# HT_SINE: Ehlers Hilbert Transform SineWave (also known as SINE)
> *The Hilbert sine wave renders cycle timing visible — crossovers of sine and lead-sine mark turning points.*
| Property | Value |
| ---------------- | -------------------------------- |
| **Category** | Cycle |
@@ -70,27 +72,6 @@ $O(P)$ per bar where $P$ is the smoothed period (typically 6-50), due to the DFT
All constants are fixed by the TA-Lib specification.
### Pseudo-code
```
function HT_SINE(source):
// Full Hilbert cascade (same as HT_DCPHASE)
// Produces: smoothPeriod, smoothPriceBuf, dcPhase
for each bar (after warmup):
// Phase from DFT accumulation (see HT_DCPHASE)
φ ← computeDCPhase(smoothPeriod, smoothPriceBuf)
// Convert phase to radians
φ_rad ← φ · (π / 180)
// Dual sine output
sine ← sin(φ_rad)
leadSine ← sin(φ_rad + π/4) // 45° lead
emit sine, leadSine
```
### Crossover Signals
| Pattern | Signal |
+2 -35
View File
@@ -1,5 +1,7 @@
# LUNAR: Lunar Phase Indicator
> *The lunar cycle maps the Moon's phase onto price — an ancient rhythm tested against modern markets.*
| Property | Value |
| ---------------- | -------------------------------- |
| **Category** | Cycle |
@@ -82,41 +84,6 @@ $O(1)$ per timestamp. No state required (deterministic from time). Zero warmup.
The calculation is entirely determined by the input timestamp.
### Pseudo-code
```
function LUNAR(timestamp):
// Julian date
JD ← timestamp_to_unix_ms / 86400000 + 2440587.5
T ← (JD - 2451545.0) / 36525.0
// Mean orbital elements (Horner evaluation)
Lp ← FMA(T, FMA(T, FMA(T, 1/538841, -0.0015786), 481267.88123421), 218.3164477)
D ← FMA(T, FMA(T, FMA(T, 1/545868, -0.0018819), 445267.1114034), 297.8501921)
M ← FMA(T, FMA(T, -0.0001536, 35999.0502909), 357.5291092)
Mp ← FMA(T, FMA(T, 0.0087414, 477198.8675055), 134.9633964)
F ← FMA(T, FMA(T, -0.0036539, 483202.0175233), 93.2720950)
// Normalize to [0°, 360°)
Lp, D, M, Mp, F ← mod(*, 360)
// Perturbation correction (6 major terms)
Σ ← 6288016·sin(Mp) + 1274242·sin(2D - Mp) + 658314·sin(2D)
+ 214818·sin(2Mp) + 186986·sin(M) + 109154·sin(2F)
λ_moon ← Lp + Σ / 1e6
// Solar longitude (simplified)
L0 ← 280.46646 + 36000.76983·T
M_sun ← 357.52911 + 35999.05029·T
λ_sun ← L0 + 1.9146·sin(M_sun) + 0.02·sin(2·M_sun)
// Phase angle and illumination
ψ ← λ_moon - λ_sun
k ← (1 - cos(ψ)) / 2
emit k // 0.0 = New Moon, 1.0 = Full Moon
```
### Output Interpretation
| Value | Phase |
+2 -30
View File
@@ -1,5 +1,7 @@
# SOLAR: Solar Cycle Indicator
> *Solar cycles encode the Sun's rhythmic activity into a tradeable signal, bridging astrophysics and price action.*
| Property | Value |
| ---------------- | -------------------------------- |
| **Category** | Cycle |
@@ -74,36 +76,6 @@ $O(1)$ per timestamp. No state required. Zero warmup. The tropical year is appro
The calculation is entirely determined by the input timestamp.
### Pseudo-code
```
function SOLAR(timestamp):
// Julian date
JD ← timestamp_to_unix_ms / 86400000 + 2440587.5
T ← (JD - 2451545.0) / 36525.0
// Geometric mean longitude
L0 ← FMA(T, FMA(T, 0.0003032, 36000.76983), 280.46646)
L0 ← mod(L0, 360)
// Mean anomaly
M ← FMA(T, FMA(T, -0.0001537, 35999.05029), 357.52911)
M ← mod(M, 360)
// Equation of center
C ← FMA(T, FMA(T, -0.000014, -0.004817), 1.914602) · sin(M)
+ FMA(T, -0.000101, 0.019993) · sin(2M)
+ 0.000289 · sin(3M)
// True ecliptic longitude
λ ← L0 + C
// Seasonal index
solar ← sin(λ · π / 180)
emit solar
```
### Seasonal Correspondence (Northern Hemisphere)
| Date (approx.) | $\lambda_{Sun}$ | Solar Value | Season |
+2 -44
View File
@@ -1,5 +1,7 @@
# SSFDSP: Ehlers SSF Detrended Synthetic Price
> *SSF-based detrended synthetic price applies a super smoother before extracting cycles, achieving cleaner periodicity isolation.*
| Property | Value |
| ---------------- | -------------------------------- |
| **Category** | Cycle |
@@ -70,50 +72,6 @@ $O(1)$ per bar. Two independent 2-pole IIR filters with $O(1)$ memory. Warmup: a
The SSF has $-3$ dB attenuation at the cutoff period, $-12$ dB/octave rolloff (2-pole), and zero phase lag at the cutoff. This is equivalent to a critically-damped Butterworth filter.
### Pseudo-code
```
function SSFDSP(source, period):
pFast ← max(2, round(period / 4))
pSlow ← max(3, round(period / 2))
// Fast SSF coefficients
αf ← √2·π / pFast
c2f ← 2·exp(-αf)·cos(αf)
c3f ← -exp(-2·αf)
c1f ← 1 - c2f - c3f
// Slow SSF coefficients
αs ← √2·π / pSlow
c2s ← 2·exp(-αs)·cos(αs)
c3s ← -exp(-2·αs)
c1s ← 1 - c2s - c3s
ssfFast_1 ← 0; ssfFast_2 ← 0
ssfSlow_1 ← 0; ssfSlow_2 ← 0
p_prev ← 0
for each price in source:
// Input averaging
avg ← (price + p_prev) / 2
// Fast SSF update
ssfFast ← c1f·avg + c2f·ssfFast_1 + c3f·ssfFast_2
// Slow SSF update
ssfSlow ← c1s·avg + c2s·ssfSlow_1 + c3s·ssfSlow_2
// SSFDSP
ssfdsp ← ssfFast - ssfSlow
// Shift state
ssfFast_2 ← ssfFast_1; ssfFast_1 ← ssfFast
ssfSlow_2 ← ssfSlow_1; ssfSlow_1 ← ssfSlow
p_prev ← price
emit ssfdsp
```
### DSP vs SSFDSP
| Aspect | DSP (EMA-based) | SSFDSP (Super-Smoother) |