FDI-Adaptive, Jurik-Filtered, TMA with Price Zones

Category: Indicators By: Iván González Created: April 28, 2026, 1:36 PM
April 28, 2026, 1:36 PM
Indicators
2 Comments

Introduction

Loxx’s FDI-Adaptive, Jurik-Filtered, TMA with Price Zones is a trend indicator that wraps three distinct ideas into a single moving average:

  1. The averaging window is not fixed — it expands and contracts every bar according to the Fractal Dimension Index (FDI) of the input series. Trending price action shrinks the window; choppy, fractal-noisy action expands it.
  2. The averaging itself is a Triangular Moving Average (TMA) — a double-smoothed weighted average that emphasises the centre of the window — applied with the variable length from step 1.
  3. The TMA’s output is then run through a Jurik Moving Average (JMA) filter to remove residual noise without adding the lag that an extra EMA would introduce.

 

On top of the line, two adaptive bands are drawn at TMA ± dev × triangular_range, providing visual price zones that scale with recent intra-bar dispersion. A signal line (TMA[1]) crosses the main line on every direction change, producing buy/sell triangles.

 

Theory Behind the Indicator

1. Fractal Dimension Index — Adaptive Window Length

The Fractal Dimension Index measures how “rough” a price series looks over a window. A perfectly straight trending line has FDI = 1; a completely random walk approaches FDI = 2. Real markets sit in between, and the value moves around as the regime changes.

 

The classical formula sums the Euclidean lengths of the normalised price path over the window:

fmax = highest(src, N)
fmin = lowest(src, N)
length = 0
for i = 1 to N - 1:
    diffCurr = (src[i-1] - fmin) / (fmax - fmin)
    diffPrev = (src[i]   - fmin) / (fmax - fmin)
    length += sqrt((diffCurr - diffPrev)^2 + 1/N^2)
fdi = 1 + (log(length) + log(2)) / log(2 * N)

 

Once the FDI is known, the indicator derives an adaptive period from it:

traildim = 1 / (2 - fdi)        // grows as the market becomes noisier
alpha    = traildim / 2          // half of that
fdiPer   = round(speed * alpha)  // final adaptive length

 

The intuition: when the market trends cleanly (fdi → 1), traildim → 1 and alpha → 0.5, so the adaptive length collapses to about half of the user’s speed parameter — the average becomes responsive. When the market goes fractal-noisy (fdi → 1.7+), traildim and alpha grow super-linearly, and the window stretches out to filter the chop.

 

2. Triangular Moving Average with Adaptive Length

The TMA is a weighted average whose weights form a triangle, peaking at the most recent bar and decreasing linearly into the past:

sumv = (N + 1) × src
sumw = (N + 1)
k    = N
for j = 1 to N:
    sumv += k × src[j]
    sumw += k
    k    -= 1
tma = sumv / sumw

 

This is mathematically equivalent to two cascaded SMAs, but more compact. Compared to a plain SMA of the same length, the TMA front-loads the weight onto recent bars while still giving older bars meaningful representation — a smoother response without the artificial flatness of a simple double-smoothing.

 

The interesting detail is N: it is fdiPer from the previous step, which changes every bar. ProBuilder allows FOR j = 1 TO N where N is a runtime variable, so this loop translates directly without contortions.

3. Jurik Moving Average — Final Smoothing

The TMA output is fed into a full Jurik filter. The Jurik MA is a four-stage smoother with adaptive volatility tracking:

 

  • Stage 1: an adaptive band tracker (bsmax, bsmin) measures the volatility regime by tracking how far the input deviates from its own running max/min envelope.
  • Stage 2: the deviation is normalised to a power-of-volatility (pow2) that drives the speed of the filter — high volatility makes the filter faster, low volatility makes it slower.
  • Stage 3: an EMA-with-adaptive-alpha smooths the input.
  • Stage 4: a phase-corrected detector adds a controlled lead/lag, tunable via the phase input (the default of 0 is neutral).

 

The full Jurik block runs to ~50 lines of ProBuilder code; the canonical implementation is taken from the existing PRC_Jurik DMX Histogram translation and renamed with the suffix jk to keep its variables isolated. Applied on top of the TMA, the JMA removes the residual jitter that the TMA cannot eliminate on its own — particularly during transitions, when the TMA’s window length itself is changing.

 

4. Price Zones — Adaptive Bands

The bands are computed as TMA ± dev × rng, where rng is a triangular-weighted measure of the recent high-low spread:

lsum = (rngper + 1) × low
hsum = (rngper + 1) × high
sumw = (rngper + 1)
k    = rngper
for j = 1 to rngper:
    lsum += k × low[j]
    hsum += k × high[j]
    sumw += k
    k    -= 1
rng = (hsum - lsum) / sumw

 

This is the same triangular weighting as the TMA but applied to the high-low channel. Because it tracks the width of the window rather than the centre, it gives a smoother, more representative volatility measure than ATR over the same span — particularly on instruments that gap.

 

Key Features at a Glance

| Feature | Behaviour |
|---|---|
| Adaptive window length | Driven by FDI of the source series; trends shrink the window, noise expands it |
| Triangular weighting | Front-loads recent bars without losing the contribution of older ones |
| Jurik post-smoothing | Removes residual jitter, especially during regime changes |
| Volatility-aware bands | `TMA ± dev × triangular_range` — channel width adapts with the asset |
| Crossover signals | `TMA crosses TMA[1]` — direction change events |
| Bar coloring | Optional, via `drawcandle` — green when TMA is rising, red when falling |

 

How to Read the Indicator

 

  1. The TMA line is the primary read. When it is rising and coloured green, the Jurik-filtered, FDI-adapted average of the price is in an uptrend. When falling and red, downtrend. The colour is binary: green if TMA > TMA[1], red otherwise. The crossings are the signal events.
  2. The two grey bands are not signals — they are context. They show how wide the recent triangular range has been. A narrow band cluster means low recent intra-bar dispersion (often a coiling phase before a move); a wide cluster means active two-way trade. Price tagging the upper band in an uptrend is healthy; price slicing through it cleanly often precedes acceleration.
  3. The triangles mark the bars on which the TMA changed direction. They are mechanical, frequent in choppy phases and rare in clean trends. Treat them as alerts, not entries — the bar after a triangle is when the trend’s direction is confirmed.
  4. The bar coloring (optional) tints each candle the same colour as the TMA line on that bar. It makes the regime visible at a glance even on a zoomed-out chart, but it can compete visually with other indicators or with the platform’s native up/down bar colouring. Disable it (colorbars = 0) if you have a busy chart.

Practical Applications

 

  1. Trend filter. Use the TMA’s slope (or its colour) as a regime gate for an entry system. The FDI adaptation means the same parameter set (default per = 30, speed = 20) tends to behave consistently across instruments — the window self-tunes to the asset’s character.
  2. Pullback entries. In an uptrend (rising green TMA), buys on touches of the lower band have a reasonable expectancy because the band width is itself volatility-adjusted — a touch of the lower band represents a similar relative pullback across asset classes.
  3. Trend exhaustion. A TMA still rising while price flatlines along the upper band, followed by the TMA’s slope turning over, is a textbook “engine cooling” pattern. The triangle that fires when the crossover finally happens marks the regime change cleanly.
  4. Timeframe pairing. Run the indicator on two timeframes (e.g. daily for regime, hourly for entries). Trade only in the direction of the daily TMA. The Jurik post-smoothing keeps the higher-timeframe line stable enough to be a usable filter.

Indicator Configuration

| Parameter | Default | Description |
|---|---|---|
| `src` | `(high+low)/2` | Source series. Use `close` for a more reactive line. |
| `per` | 30 | Window length over which the FDI is computed. Larger `per` = smoother, slower-adapting FDI. |
| `speedin` | 20 | Base length the FDI adaptation scales. Higher = generally larger windows. |
| `smthper` | 30 | Jurik smoothing length applied to the TMA output. |
| `smthphs` | 0 | Jurik phase: −100 to +100. Negative biases lag, positive biases lead. 0 is neutral. |
| `rngper` | 5 | Window for the triangular range (bands). Short by default — the bands react fast. |
| `dev` | 1.8 | Band multiplier. 1.8 keeps most price action inside the bands; raise for an envelope, lower for a tight channel. |
| `colorbars` | 1 | 1 = recolour candles by TMA direction; 0 = leave the platform's native colouring. |
| `showsignals` | 1 | 1 = draw triangles on direction changes; 0 = TMA line only. |

 

Code

 

// PRC_FDI-Adaptive Jurik-Filtered TMA w/ Price Zones
// version = 0
// 28.04.26
// Ivan Gonzalez @ www.prorealcode.com
// Sharing ProRealTime knowledge
// Original: FDI-Adaptive, Jurik-Filtered, TMA w/ Price Zones [Loxx] (TradingView)
//////////////////////////////////////////////////////////////
// Inputs
src = (high+low)/2
per = 30
speedin = 20
smthper = 30
smthphs = 0.0
rngper = 5
dev = 1.8
colorbars = 1
showsignals = 1
//////////////////////////////////////////////////////////////
// === FDI calculation — period adaptive via Fractal Dimension Index
fmax = highest[per](src)
fmin = lowest[per](src)


IF fmax > fmin THEN
    fdiLen = 0
    FOR i = 1 TO per - 1 DO
        diffCurr = (src[i-1] - fmin) / (fmax - fmin)
        diffPrev = (src[i] - fmin) / (fmax - fmin)
        fdiLen = fdiLen + sqrt((diffCurr - diffPrev)*(diffCurr - diffPrev) + 1/(per*per))
    NEXT
    IF fdiLen > 0 THEN
        fdi = 1 + (log(fdiLen) + log(2)) / log(2 * per)
    ELSE
        fdi = 1.5
    ENDIF
ELSE
    fdi = 1.5
ENDIF


IF fdi < 2 THEN
    traildim = 1 / (2 - fdi)
ELSE
    traildim = 1
ENDIF
alphaFdi = traildim / 2
fdiper = round(speedin * alphaFdi)
IF fdiper < 1 THEN
    fdiper = 1
ENDIF
IF fdiper > 200 THEN
    fdiper = 200
ENDIF
//////////////////////////////////////////////////////////////
// === Triangular weighted MA with adaptive period
sumv = (fdiper + 1) * src
sumw = (fdiper + 1)
k = fdiper
FOR j = 1 TO fdiper DO
    sumv = sumv + k * src[j]
    sumw = sumw + k
    k = k - 1
NEXT
tmaRaw = sumv / sumw
//////////////////////////////////////////////////////////////
// === Jurik MA filter applied to tmaRaw
len = max(smthper, 1)
phs = smthphs
len1 = max(log(sqrt(0.5*(len-1)))/log(2)+2, 0)
pow1 = max(len1-2, 0.5)
avglen = 65


IF barindex < avglen+1 THEN
    Filt0jk = tmaRaw
    Filt1jk = tmaRaw
    juriktma = tmaRaw
    bsmaxjk = 0
    bsminjk = 0
    vsumjk = 0
    avoltyjk = 0
ELSE
    del1jk = tmaRaw - bsmaxjk[1]
    del2jk = tmaRaw - bsminjk[1]
    div = 1/(10+10*(min(max(len-10,0),100))/100)
    IF abs(del1jk) > abs(del2jk) THEN
        voltyjk = abs(del1jk)
    ELSE
        voltyjk = abs(del2jk)
    ENDIF
    vsumjk = vsumjk[1] + div*(voltyjk - voltyjk[10])


    tempavgjk = average[avglen](vsumjk)


    yjk = barindex+1
    IF yjk <= avglen+1 THEN
        avoltyjk = avoltyjk[1] + 2*(vsumjk-avoltyjk[1])/(avglen+1)
    ELSE
        avoltyjk = tempavgjk
    ENDIF


    IF avoltyjk > 0 THEN
        dvoltyjk = voltyjk/avoltyjk
    ELSE
        dvoltyjk = 0
    ENDIF


    IF dvoltyjk > pow(len1,1/pow1) THEN
        dvoltyjk = pow(len1,1/pow1)
    ENDIF
    IF dvoltyjk < 1 THEN
        dvoltyjk = 1
    ENDIF


    pow2jk = pow(dvoltyjk, pow1)
    len2 = sqrt(0.5*(len-1))*len1
    kvjk = pow(len2/(len2+1), sqrt(pow2jk))


    IF del1jk > 0 THEN
        bsmaxjk = tmaRaw
    ELSE
        bsmaxjk = tmaRaw - kvjk*del1jk
    ENDIF


    IF del2jk < 0 THEN
        bsminjk = tmaRaw
    ELSE
        bsminjk = tmaRaw - kvjk*del2jk
    ENDIF


    IF phs < -100 THEN
        phaseratio = 0.5
    ELSIF phs > 100 THEN
        phaseratio = 2.5
    ELSE
        phaseratio = phs/100+1.5
    ENDIF


    beta = 0.45 * (len - 1) / (0.45 * (len - 1) + 2)
    alphajk = pow(beta, pow2jk)


    Filt0jk = (1 - alphajk) * tmaRaw + alphajk * Filt0jk[1]
    Det0jk = (tmaRaw - Filt0jk) * (1 - beta) + beta * Det0jk[1]
    Filt1jk = Filt0jk + phaseratio * Det0jk
    Det1jk = (Filt1jk - juriktma[1]) * ((1 - alphajk) * (1 - alphajk)) + (alphajk * alphajk) * Det1jk[1]
    juriktma = juriktma[1] + Det1jk
ENDIF


tma = juriktma
sig = tma[1]
//////////////////////////////////////////////////////////////
// === Range calculation (calcrng) — triangular weighted high-low spread
lsum = (rngper + 1) * low
hsum = (rngper + 1) * high
sumwr = (rngper + 1)
kr = rngper
FOR jr = 1 TO rngper DO
    lsum = lsum + kr * low[jr]
    hsum = hsum + kr * high[jr]
    sumwr = sumwr + kr
    kr = kr - 1
NEXT
rng = (hsum - lsum) / sumwr
//////////////////////////////////////////////////////////////
// === Price zones
uplvl = tma + dev * rng
dnlvl = tma - dev * rng
//////////////////////////////////////////////////////////////
// === Color (TMA up/down)
IF tma > sig THEN
    r = 45
    g = 210
    b = 4
ELSE
    r = 210
    g = 4
    b = 45
ENDIF
//////////////////////////////////////////////////////////////
// === Color bars (replica barcolor de Pine)
IF colorbars = 1 THEN
    drawcandle(open,high,low,close) coloured(r,g,b)
ENDIF
//////////////////////////////////////////////////////////////
// === Signals
goLong = tma CROSSES OVER sig
goShort = tma CROSSES UNDER sig


IF showsignals = 1 AND goLong THEN
    drawtext("▲", barindex, low - 0.3*rng) coloured(0,255,255)
ENDIF
IF showsignals = 1 AND goShort THEN
    drawtext("▼", barindex, high + 0.3*rng) coloured(255,0,255)
ENDIF
//////////////////////////////////////////////////////////////
return tma coloured(r,g,b) style(line,3) as "TMA", uplvl coloured(120,120,120) as "Upper Channel", dnlvl coloured(120,120,120) as "Lower Channel"

Download
Filename: PRC_FDI-Adaptive-Jurik-Filter.itf
Downloads: 25
Iván González Master
Code artist, my biography is a blank page waiting to be scripted. Imagine a bio so awesome it hasn't been coded yet.
Author’s Profile

Comments

Logo Logo
Loading...