A moving average cross tells you the trend changed. It does not tell you where to act. By the time two averages cross, price has usually already moved, and the cross itself gives you no level, no invalidation and no way to re enter if you missed it.
Zero-Lag MA Trend Levels, by ChartPrime, closes that gap. It crosses a zero lag average against a plain EMA of the same period, and on every cross it stamps a zone one ATR deep onto the chart, anchored to the average itself. That zone becomes the working level for the whole leg: support underneath price in an uptrend, resistance above it in a downtrend. It carries the price at which the cross happened, it extends forward as long as the trend lasts, and when price eventually clears it in the direction of the trend, the indicator prints a mark.
So instead of one event you get three: a regime change, a zone to trade against, and a continuation signal inside the move.
Every moving average lags because it averages the past. The trick here is to feed the average a series that is deliberately pushed forward by exactly the amount the average will pull it back:
ema = EMA(close, length)
correction = close + (close - ema)
zlma = EMA(correction, length)
Read the middle line carefully. The term close - ema is how far price currently sits above or below its own average, which is precisely the lag. Adding that distance back to price produces a mirror image of the average on the other side of price. When the second EMA smooths that mirrored series, the lag it introduces cancels against the lead already built in, and the resulting curve sits on price instead of trailing behind it.
This is worth separating from the other two constructions that share the name. Ehlers style zero lag shifts the price series in time and reduces lag by looking back a fixed number of bars. Zero lag built from a double TEMA cascade applies the same filter twice and combines the outputs. This one works in the price dimension rather than the time dimension: it reflects price across its own average. Different maths, different behaviour around turns, and it matters in practice, because the reflection amplifies noise in a flat market while the time shift does not.
The regime is simply which side of the EMA the zero lag curve sits on. Because the zero lag curve reacts first, it crosses the slower EMA early in a move, and that cross is the event.
On every cross a zone is created, one ATR(200) deep, anchored to the zero lag curve:
zlma - ATR up to zlma. It is prospective support.zlma up to zlma + ATR. It is prospective resistance.Two design choices give the zone its character. It is anchored at the moment of the cross and then frozen in price, so it becomes a static horizontal reference rather than a trailing band, which is what makes it usable as a level. And its depth comes from a long, slow ATR(200) rather than a fast one, so the zone thickness reflects the instrument normal volatility rather than the volatility of the last few bars. A single wild session does not inflate it.
Each zone keeps extending to the right for as long as the regime holds, and stops the moment the next cross opens a new one. The price at which the cross occurred is printed on it.
The curve and the ribbon carry independent information, which is easy to miss:
When the two disagree, the curve has started to roll over while the regime has not flipped yet. That divergence between the two colour layers is the earliest warning the indicator gives.
The last layer is the one most people overlook, and it is the most tradeable.
Once a regime is established and the signal bar and the one after it are out of the way, the indicator watches how price interacts with the live zone. It prints a mark when price clears the zone decisively in the direction of the trend, meaning the entire candle is on the right side of it: the low above the top of a support zone, or the high below the bottom of a resistance zone. A wick poking through is not enough.
That is a pullback that has been tested and rejected, not a fresh signal. Price came back into the zone, the zone held, and the candle closed the argument.
length = 15: period of both the zero lag curve and the EMA. Lower reacts faster and crosses more often, higher filters harder. Both averages always share it, which is what keeps the cross meaningful.showLevl = 1: set to 0 to hide the zones and keep the two curves and the ribbon on their own.maxBox = 40: how many past zones stay on the chart.maxMark = 60: how many continuation triangles stay on the chart.The ATR that sets the zone depth is fixed at 200 periods, matching the original. On very short intraday timeframes 200 bars covers a long stretch of session, so the zone may look wide relative to recent ranges. That is intentional: the zone is meant to represent the instrument habitual volatility, not the volatility of the last hour.
Two limits deserve a word. The original keeps up to 500 objects on the chart; this version keeps 40 zones and 60 marks, which is what a chart can display legibly and what keeps the redraw fast. Raise them if you want deeper history.
//-------------------------------------//
//PRC_Zero-Lag MA Trend Levels
//version = 0
//22.09.26
//Iván González @ www.prorealcode.com
//Author: ChartPrime
//Sharing ProRealTime knowledge
//-------------------------------------//
DEFPARAM drawonlastbaronly = true
//-------------------------------------//
// Inputs
//-------------------------------------//
length = 15 // Length of both moving averages
showLevl = 1 // 1 = draw the trend level boxes, 0 = hide them
maxBox = 40 // How many past levels are kept on the chart
maxMark = 60 // How many retest marks are kept on the chart
// Bullish / bearish colours
upR = 48
upG = 212
upB = 83
dnR = 64
dnG = 67
dnB = 241
//-------------------------------------//
// Zero-Lag MA
// The correction mirrors price across its own EMA, so the second
// EMA lands on price instead of trailing behind it.
//-------------------------------------//
src = customclose
emaValue = average[length,1](src)
correction = src + (src - emaValue)
zlma = average[length,1](correction)
atrV = averagetruerange[200](close)
signalUp = zlma crosses over emaValue
signalDn = zlma crosses under emaValue
IF signalUp OR signalDn THEN
chkSig = 1
ELSE
chkSig = 0
ENDIF
//-------------------------------------//
// Colours
//-------------------------------------//
// ZLMA is coloured by its own slope over the last 3 bars. When it is
// flat the original leaves a gap; here the last colour is kept so the
// line stays continuous.
ONCE zr = upR
ONCE zg = upG
ONCE zb = upB
IF zlma > zlma[3] THEN
zr = upR
zg = upG
zb = upB
ELSIF zlma < zlma[3] THEN
zr = dnR
zg = dnG
zb = dnB
ENDIF
// The EMA and the ribbon are coloured by which side of the ZLMA it sits on
IF emaValue < zlma THEN
er = upR
eg = upG
eb = upB
ELSE
er = dnR
eg = dnG
eb = dnB
ENDIF
COLORBETWEEN(zlma, emaValue, er, eg, eb, 51)
//-------------------------------------//
// Trend levels
// A box is opened on every crossover and keeps extending 4 bars to the
// right until the next signal opens a new one. Boxes are stored instead
// of drawn on the spot, so past levels survive the repaint.
//-------------------------------------//
ONCE nBox = 0
ONCE nMark = 0
IF showLevl = 1 AND chkSig = 1 THEN
// freeze the box that was still alive: its last extension was one bar ago
IF nBox > 0 THEN
$bxX2[nBox] = barindex + 3
ENDIF
WHILE nBox >= maxBox DO
FOR i = 1 TO nBox - 1 DO
$bxX1[i] = $bxX1[i+1]
$bxX2[i] = $bxX2[i+1]
$bxTop[i] = $bxTop[i+1]
$bxBot[i] = $bxBot[i+1]
$bxDir[i] = $bxDir[i+1]
$bxPx[i] = $bxPx[i+1]
NEXT
nBox = nBox - 1
WEND
nBox = nBox + 1
$bxX1[nBox] = barindex
$bxX2[nBox] = barindex + 4
$bxPx[nBox] = round(close * 100) / 100
IF signalUp THEN
// support zone hanging below the ZLMA
$bxTop[nBox] = zlma
$bxBot[nBox] = zlma - atrV
$bxDir[nBox] = 1
ELSE
// resistance zone sitting above the ZLMA
$bxTop[nBox] = zlma + atrV
$bxBot[nBox] = zlma
$bxDir[nBox] = -1
ENDIF
ENDIF
// Edges of the box that is currently alive
IF nBox > 0 THEN
liveTop = $bxTop[nBox]
liveBot = $bxBot[nBox]
ENDIF
//-------------------------------------//
// Retest marks
// Price failing back through the live zone, once the signal bar and the
// one right after it are out of the way.
//-------------------------------------//
IF showLevl = 1 AND nBox > 0 AND chkSig = 0 AND chkSig[1] = 0 THEN
mkHit = 0
IF high crosses under liveBot AND emaValue > zlma THEN
mkHit = -1
mkY = high[1]
ELSIF low crosses over liveTop AND emaValue < zlma THEN
mkHit = 1
mkY = low[1]
ENDIF
IF mkHit <> 0 THEN
WHILE nMark >= maxMark DO
FOR i = 1 TO nMark - 1 DO
$mkX[i] = $mkX[i+1]
$mkY[i] = $mkY[i+1]
$mkDir[i] = $mkDir[i+1]
NEXT
nMark = nMark - 1
WEND
nMark = nMark + 1
$mkX[nMark] = barindex - 1
$mkY[nMark] = mkY
$mkDir[nMark] = mkHit
ENDIF
ENDIF
//-------------------------------------//
// Drawing
//-------------------------------------//
IF islastbarupdate THEN
IF nBox > 0 THEN
FOR i = 1 TO nBox DO
IF i = nBox THEN
xr = barindex + 4 // the live box is still extending
ELSE
xr = $bxX2[i]
ENDIF
IF $bxDir[i] = 1 THEN
br = upR
bg = upG
bb = upB
dY = $bxTop[i]
ELSE
br = dnR
bg = dnG
bb = dnB
dY = $bxBot[i]
ENDIF
DRAWRECTANGLE($bxX1[i], $bxTop[i], xr, $bxBot[i]) COLOURED(br,bg,bb,255) FILLCOLOR(br,bg,bb,26)
// Price tag as plain text: a box measured in bars cannot hold a
// label measured in pixels once you zoom out.
pxTag = $bxPx[i]
xTag = round(($bxX1[i] + xr) / 2)
yTag = ($bxTop[i] + $bxBot[i]) / 2
DRAWTEXT("#pxTag#", xTag, yTag) COLOURED(br,bg,bb,255)
// Signal diamond, on the ZLMA side of the zone
DRAWTEXT("◆", $bxX1[i], dY) COLOURED(br,bg,bb,255)
NEXT
ENDIF
IF nMark > 0 THEN
FOR i = 1 TO nMark DO
IF $mkDir[i] = 1 THEN
DRAWTEXT("▲", $mkX[i], $mkY[i]) COLOURED(upR,upG,upB,255)
ELSE
DRAWTEXT("▼", $mkX[i], $mkY[i]) COLOURED(dnR,dnG,dnB,255)
ENDIF
NEXT
ENDIF
ENDIF
//-------------------------------------//
RETURN zlma AS "Zero-Lag MA" STYLE(line,2) COLOURED(zr,zg,zb), emaValue AS "EMA" STYLE(line,1) COLOURED(er,eg,eb)
The idea behind Zero-Lag MA Trend Levels is small and useful: a moving average cross is an event, and events are worth remembering as levels. By freezing an ATR deep zone at every cross and keeping it alive for the whole leg, the indicator converts a signal that is usually gone in one bar into a reference that stays tradeable for weeks. Add the two colour layers for momentum against regime, and the continuation marks for pullbacks that held, and what starts as a simple cross of two averages ends up as a complete framework: where the trend turned, where to enter, and where you are wrong.