Responsive, Non-Repainting Laguerre ZigZag For ThinkOrSwim

whoDAT

Member
Plus
I'm a big fan of ZigZag indicators. A general problem is they are repainters.

A notable exception is the QQE ZigZag, an indicator that I find very useful.
https://usethinkscript.com/threads/...ncl-qqe-non-repainting-for-thinkorswim.12546/

Laguerre filters are fast-acting methods to smooth inputs. John Eulers described a fast Laguerre method to smooth financial market data in his article entitled "Time Warp - Without Space Travel". Speed is good; speed with market data is fast money.
https://www.mesasoftware.com/papers/TimeWarp.pdf

The below code uses a Eulers High-Low Laguerre Filter (EHLLF) to find uptrends and downtrends, creating a responsive, non-repainting ZigZag.

The plots include the LaguerreZigZag, along with Eulers' Filter and Finite ImpulseResponse (FIR).

All plots except the LaguerreZigZag work in TOS Mobile apps (due to the EnableApproximation function). Use the UpSignal and DownSignal with Boolean value, up or down arrows to see where the Zig Zags.

The image shows the QQE ZigZag on the left and the LaguerreZigZag on the right. Both for SPX, 5 minute time frame for the last 5 days.

LaguerreZZ.jpg


Enjoy!

Code:
# Laguerre ZigZag - A responsive, non-repainting ZigZag indicator
# Authored by whoDAT - April 1, 2025

# A HL2 input is typically better to reduce the number of zigs and zags.
# The Gamma is the dampening function.
#  - Increasing Gamma values (approaching 1.0) will dampen the input more, reducing the number of zigs and zags.
#  - Lower Gamma values (approaching 0.0) will make a noisy output with more zigs and zags.
# The LowHighLookBack defines the how far back to look for the lowest or highest value to use when painting the ZigZag line.

input Price = HL2;
input Gamma = 0.80;
input LowHighLookback = 10;

# Laguerre filter
def L0 = (1 - Gamma) * Price + (Gamma * L0[1]);
def L1 = (-1 * Gamma * L0) + L0[1] + (Gamma * L1[1]);
def L2 = (-1 * Gamma * L1) + L1[1] + (Gamma * L2[1]);
def L3 = (-1 * Gamma * L2) + L2[1] + (Gamma * L3[1]);

def Filt = (L0 + (2 * L1) + (2 * L2) + L3) / 6;
def FIR = (Price + (2 * Price[1]) + (2 * Price[2]) + Price[3]) / 6;

# If the Laguerre filter is increasing (or decreasing), this is an uptick (downtick). That is used to make the upward or downward signal to find the zigs and zags.
def Uptick = if (L0 > Filt) then 1 else Double.NaN;
def Downtick = if (L0 < Filt) then 1 else Double.NaN;

def UpSig = if Downtick[1] == 1 and Uptick[0] == 1 then Lowest(low, LowHighLookback) else Double.NaN;
def DnSig = if Uptick[1] == 1 and Downtick[0] == 1 then Highest(high, LowHighLookback) else Double.NaN;

def ZZL = if !IsNaN(UpSig) then UpSig else if !IsNaN(DnSig)  then DnSig else Double.NaN;

plot ZigZagLaguerre = ZZL;
ZigZagLaguerre.EnableApproximation();

plot FilterResponse = Filt;
plot FiniteImpulseResponse = FIR;
plot UpSignal = UpSig;
plot DownSignal = DnSig;

# END OF CODE
 
Last edited:

Join useThinkScript to post your question to a community of 21,000+ developers and traders.

Hello,

I am confused on how to interpret the use of the indicator correctly (also I am not a coder). Does this indicator "signal" when price action crosses above/below the zig-zag? That is what my smooth brain is telling me.


Any comments or links on how to interpret would be appreciated. Thanks.
 
Ok. Indicator does not work how I originally thought. Is there a way to add "displacement code" to this indicator to shift the "zig zag" line closer to the candles (displace to the left)?
 
Hello,

I am confused on how to interpret the use of the indicator correctly (also I am not a coder). Does this indicator "signal" when price action crosses above/below the zig-zag? That is what my smooth brain is telling me.


Any comments or links on how to interpret would be appreciated. Thanks.
Pooze.

ZigZags are trend indicators, showing us the general direction of a market. This indicator shows when the market trend is up or down - when to buy or sell. For the Laguerre ZigZag - the top peak shows a good indicator of when to sell; and the bottom trough shows a good indicator when to buy.

Ok. Indicator does not work how I originally thought. Is there a way to add "displacement code" to this indicator to shift the "zig zag" line closer to the candles (displace to the left)?
There's little to be gained for non-repainting indicators by displacing them back in time. We can't go back in time to affect decisions to enter a trade. You may be more interested in a repainting indicator such as ZigZagHighLow.
 
Last edited by a moderator:
Does this tell us anything different than the price action plot? Isn't this just replotting price action except with a lag? Trying to figure this out.... maybe Elliot wave counting? or maybe ABC pattern recognitions? maybe better visuals for highs and lows for FIB breakouts? Overall trend clarity maybe?
 
Does this tell us anything different than the price action plot? Isn't this just replotting price action except with a lag? Trying to figure this out.... maybe Elliot wave counting? or maybe ABC pattern recognitions? maybe better visuals for highs and lows for FIB breakouts? Overall trend clarity maybe?

No, zigzag does not provide price action. It is a trend indicator.
Meaning that it complements price action. Provides overall context to the bar-by-bar action.

You are correct. No indicator should be used in isolation.
Adding price action such as ABC pattern recognitions to your chart
and
Supplementing with support and resistance such as Fibonacci Retracements is a great way to build a strategy.
read more:
https://usethinkscript.com/threads/basics-for-developing-a-good-strategy.8058/
 
Does this tell us anything different than the price action plot? Isn't this just replotting price action except with a lag? Trying to figure this out.... maybe Elliot wave counting? or maybe ABC pattern recognitions? maybe better visuals for highs and lows for FIB breakouts? Overall trend clarity maybe?

As a ZigZag indicator, it is useful to predict direction of trends. It is a non-repainter. When it says: "Go long"; it doesn't come back 20 minutes later and say: "Sorry, I didn't really mean go long".

Here's the performance for the past 5 days on SPX. 50% win rate, 6.7% return. YMMV.

(Grey zones are "flat zones" where trades aren't made. Otherwise this graph is purely the Laguerre ZigZag.)

LaguerreSPX.jpg
 
As a ZigZag indicator, it is useful to predict direction of trends. It is a non-repainter. When it says: "Go long"; it doesn't come back 20 minutes later and say: "Sorry, I didn't really mean go long".

Here's the performance for the past 5 days on SPX. 50% win rate, 6.7% return. YMMV.

(Grey zones are "flat zones" where trades aren't made. Otherwise this graph is purely the Laguerre ZigZag.)

View attachment 24616
Can you share the script for this chart ?
 
Can you share the script for this chart ?
Laguerre Zig-Zag with Profit.


Code:
# Laguerre ZigZag - A responsive, non-repainting ZigZag indicator
# Authored by whoDAT - April 1, 2025

# A HL2 input is typically better to reduce the number of zigs and zags.
# The Gamma is the a dampening function.
#  - Increasing Gamma values (approaching 1.0) will dampen the input more, reducing the number of zigs and zags.
#  - Lower Gamma values (approaching 0.0) will make a noisy output with more zigs and zags.
# The LowHighLookBack defines the how far back to look for the lowest or highest value to use when painting the ZigZag line.


input Price = HL2;
input Gamma = 0.80;
input LowHighLookback = 10;
input useRejection = yes;
input rejectPercent = 0.0001;


# Laguerre filter
def L0 = (1 - Gamma) * Price + (Gamma * L0[1]);
def L1 = (-1 * Gamma * L0) + L0[1] + (Gamma * L1[1]);
def L2 = (-1 * Gamma * L1) + L1[1] + (Gamma * L2[1]);
def L3 = (-1 * Gamma * L2) + L2[1] + (Gamma * L3[1]);

def Filt = (L0 + (2 * L1) + (2 * L2) + L3) / 6;
def FIR = (Price + (2 * Price[1]) + (2 * Price[2]) + Price[3]) / 6;

# If the Laguerre filter is increasing (or decreasing), this is an uptick (downtick). That is used to make the upward or downward signal to find the zigs and zags.

def rejUp = if useRejection then (Filt + Price * rejectPercent / 100) else Filt;
def rejDn = if useRejection then (Filt - Price * rejectPercent / 200) else Filt;

def Uptick = if (L0 >= rejUp) then 1 else 0;
def Downtick = if (L0 <= rejDn) then 1 else 0;

def UpSig = if Downtick[1] == 1 and Uptick == 1 then low else Double.NaN;
def DnSig = if Uptick[1] == 1 and Downtick == 1 then high else Double.NaN;
#def UpSig = if Downtick[1] == 1 and Uptick == 1 then Lowest(low, LowHighLookback) else Double.NaN;
#def DnSig = if Uptick[1] == 1 and Downtick == 1 then Highest(high, LowHighLookback) else Double.NaN;



def ZZL = if !IsNaN(UpSig) then UpSig else if !IsNaN(DnSig)  then DnSig else Double.NaN;

plot ZigZagLaguerre = ZZL;
ZigZagLaguerre.EnableApproximation();

plot FilterResponse = Filt;
plot FiniteImpulseResponse = FIR;
plot UpSignal = UpSig;
plot DownSignal = DnSig;

##New testing code##################################################################################


#############################################################################

#############################################################
###   Determine a flat market
#############################################################


input TradeInFlatRange = Yes;
input BarsForFlatRange = 15;
input BarsReqToStayInRange = 13;

def HH = Highest(high[1], BarsForFlatRange);
def LL = Lowest(low[1], BarsForFlatRange);
def maxH = Highest(HH, BarsReqToStayInRange);
def maxL = Lowest(LL, BarsReqToStayInRange);
def HHn = if maxH == maxH[1] or maxL == maxL then maxH else HHn[1];
def LLn = if maxH == maxH[1] or maxL == maxL then maxL else LLn[1];
def Bh = if high <= HHn and HHn == HHn[1] then HHn else Double.NaN;
def Bl = if low >= LLn and LLn == LLn[1] then LLn else Double.NaN;
def CountH = if IsNaN(Bh) or IsNaN(Bl) then 2 else CountH[1] + 1;
def CountL = if IsNaN(Bh) or IsNaN(Bl) then 2 else CountL[1] + 1;
def ExpH = if BarNumber() == 1 then Double.NaN else
            if CountH[-BarsReqToStayInRange] >= BarsReqToStayInRange then HHn[-BarsReqToStayInRange] else
            if high <= ExpH[1] then ExpH[1] else Double.NaN;
def ExpL = if BarNumber() == 1 then Double.NaN else
            if CountL[-BarsReqToStayInRange] >= BarsReqToStayInRange then LLn[-BarsReqToStayInRange] else
            if low >= ExpL[1] then ExpL[1] else Double.NaN;

plot BoxHigh = if !IsNaN(ExpL) and !IsNaN(ExpH) then ExpH else Double.NaN;
plot BoxLow = if !IsNaN(ExpL) and !IsNaN(ExpH) then ExpL else Double.NaN;

input showFlatCloud = yes;
AddCloud(if showFlatCloud then BoxHigh else Double.NaN, BoxLow, Color.GRAY, Color.GRAY);

#addcloud( BoxHigh, BoxLow, color.gray, color.gray);

def Flat = if (!IsNaN(BoxHigh[1]) and !IsNaN(BoxLow[1])) and !TradeInFlatRange then 1 else 0;

#addChartBubble(Flat==1 and isNan(Flat[1]),BoxHigh[1],"Flat Market",color.gray,yes);



############################################
##  Define BuySignal and SellSignal above
##  or uncomment them out below and set
##  them to your buy/sell conditions
##
##  If using stops, define them below
############################################

###------------------------------------------------------------------------------------------

input showSignals = yes;
input showLabels  = yes;
input showBubbles = yes;
input useStops = no;
input useAlerts = no;

###------------------------------------------------------------------------------------------

############################################
##  Create Signals -
##  FILL IN THIS SECTION IF NOT DEFINED ABOVE
##
############################################


#Laguerre ZigZag Buy Sell

def BuySignal = !IsNaN(UpSig) and !Flat ; # insert condition to create long position
def SellSignal = !IsNaN(DnSig) and !Flat; # insert condition to create short position
def BuyStop  = if !useStops then 0 else if !IsNaN(DnSig) and !Flat then 1 else 0;
def SellStop = if !useStops then 0 else if !IsNaN(UpSig) and !Flat then 1 else 0;



#def BuyStop  = if !useStops then 0 else if  state == state.long and (haOpen > haClose) and Flat then 1 else 0  ; # insert condition to stop in place of the 0<0
#def SellStop = if !useStops then 0 else if  state == state.short and (haOpen < haClose) and Flat then 1 else 0  ; # insert condition to stop in place of the 0>0

#######################################
##  Maintain the position of trades
#######################################

def CurrentPosition;  # holds whether flat = 0 long = 1 short = -1

if (BarNumber() == 1) or IsNaN(CurrentPosition[1]) {
    CurrentPosition = 0;
} else {
    if CurrentPosition[1] == 0 {            # FLAT
        if (BuySignal) {
            CurrentPosition = 1;
        } else if (SellSignal) {
            CurrentPosition = -1;
        } else {
            CurrentPosition = CurrentPosition[1];
        }
    } else if CurrentPosition[1] == 1 {      # LONG
        if (SellSignal) {
            CurrentPosition = -1;
        } else if (BuyStop and useStops) {
            CurrentPosition = 0;
        } else {
            CurrentPosition = CurrentPosition[1];
        }
    } else if CurrentPosition[1] == -1 {     # SHORT
        if (BuySignal) {
            CurrentPosition = 1;
        } else if (SellStop and useStops) {
            CurrentPosition = 0;
        } else {
            CurrentPosition = CurrentPosition[1];
        }
    } else {
        CurrentPosition = CurrentPosition[1];
    }
}


def isLong  = if CurrentPosition == 1 then 1 else 0;
def isShort = if CurrentPosition == -1 then 1 else 0;
def isFlat  = if CurrentPosition == 0 then 1 else 0;

# If not already long and get a BuySignal
plot BuySig = if (!isLong[1] and BuySignal and showSignals) then 1 else 0;
#BuySig.AssignValueColor(Color.CYAN);
BuySig.SetPaintingStrategy(PaintingStrategy.BOOLEAN_ARROW_UP);
BuySig.SetLineWeight(5);

Alert(BuySig and useAlerts, "Buy Signal", Alert.BAR, Sound.Ding);
Alert(BuySig and useAlerts, "Buy Signal", Alert.BAR, Sound.Ding);

# If not already short and get a SellSignal
plot SellSig = if (!isShort[1] and SellSignal and showSignals) then 1 else 0;
#SellSig.AssignValueColor(Color.YELLOW);
SellSig.SetPaintingStrategy(PaintingStrategy.BOOLEAN_ARROW_DOWN);
SellSig.SetLineWeight(5);

Alert(SellSig and useAlerts, "Sell Signal", Alert.BAR, Sound.Ding);
Alert(SellSig and useAlerts, "Sell Signal", Alert.BAR, Sound.Ding);

# If long and get a BuyStop
plot BuyStpSig = if (BuyStop and isLong[1] and showSignals and useStops) then 1 else 0;
BuyStpSig.AssignValueColor(Color.LIGHT_GRAY);
BuyStpSig.SetPaintingStrategy(PaintingStrategy.BOOLEAN_ARROW_DOWN);
BuyStpSig.SetLineWeight(3);

# If short and get a SellStop
plot SellStpSig = if (SellStop and isShort[1] and showSignals and useStops) then 1 else 0;
SellStpSig.AssignValueColor(Color.LIGHT_GRAY);
SellStpSig.SetPaintingStrategy(PaintingStrategy.BOOLEAN_ARROW_UP);
SellStpSig.SetLineWeight(3);


#######################################
##  Orders
#######################################

def isOrder = if CurrentPosition == CurrentPosition[1] then 0 else 1; # Position changed so it's a new order

# If there is an order, then the price is the next days close
#def orderPrice = if (isOrder and (BuySignal or SellSignal)) then close else orderPrice[1];
def orderPrice = if (isOrder and BuySignal) then high else if (isOrder and SellSignal) then low else orderPrice[1];

#######################################
##  Price and Profit
#######################################

def profitLoss;

if (!isOrder or orderPrice[1] == 0) {
    profitLoss = 0;
} else if ((isOrder and isLong[1]) and (SellSig or BuyStpSig)) {
    profitLoss = close - orderPrice[1];
} else if ((isOrder and isShort[1]) and (BuySig or SellStpSig)) {
    profitLoss = orderPrice[1] - close;
} else {
    profitLoss = 0;
}

# Total Profit or Loss

input ProfitLeverageMultiple = 1;

def profitLossSum = CompoundValue(1, if IsNaN(isOrder)  or BarNumber() == 1 then 0 else if isOrder then profitLossSum[1] + profitLoss else profitLossSum[1], 0);

# How many trades won or lost
def profitWinners = CompoundValue(1, if IsNaN(profitWinners[1]) or BarNumber() == 1 then 0 else if isOrder and profitLoss > 0 then profitWinners[1] + 1 else profitWinners[1], 0);

def profitLosers = CompoundValue(1, if IsNaN(profitLosers[1])  or BarNumber() == 1 then 0 else if isOrder and profitLoss < 0 then profitLosers[1] + 1 else profitLosers[1], 0);

def profitPush = CompoundValue(1, if IsNaN(profitPush[1])  or BarNumber() == 1 then 0 else if isOrder and profitLoss == 0 then profitPush[1] + 1 else profitPush[1], 0);

def profitLongWinners = CompoundValue(1, if IsNaN(profitLongWinners[1]) or BarNumber() == 1 then 0 else if isOrder and profitLoss > 0 and isLong[1] then profitLongWinners[1] + 1 else profitLongWinners[1], 0);

def profitShortWinners = CompoundValue(1, if IsNaN(profitShortWinners[1]) or BarNumber() == 1 then 0 else if isOrder and profitLoss > 0 and isShort[1] then profitShortWinners[1] + 1 else profitShortWinners[1], 0);

def orderCount = (profitWinners + profitLosers + profitPush) - 1;

# Current Open Trade Profit or Loss
def TradePL = if isLong then Round(((close - orderPrice) / TickSize()) * TickValue()) else if isShort then Round(((orderPrice - close) / TickSize()) * TickValue()) else 0;

# Convert to actual dollars based on Tick Value for bubbles
def dollarProfitLoss = if orderPrice[1] == 0 or IsNaN(orderPrice[1]) then 0 else Round((profitLoss / TickSize()) * TickValue() * ProfitLeverageMultiple);

# Closed Orders dollar P/L
def dollarPLSum = Round((profitLossSum / TickSize()) * TickValue() * ProfitLeverageMultiple);

# Split profits or losses by long and short trades
def profitLong = CompoundValue(1, if IsNaN(profitLong[1])  or BarNumber() == 1 then 0 else if isOrder and isLong[1] then profitLong[1] + dollarProfitLoss else profitLong[1], 0);

def profitShort = CompoundValue(1, if IsNaN(profitShort[1])  or BarNumber() == 1 then 0 else if isOrder and isShort[1] then profitShort[1] + dollarProfitLoss else profitShort[1], 0);

def countLong = CompoundValue(1, if IsNaN(countLong[1])  or BarNumber() == 1 then 0 else if isOrder and isLong[1] then countLong[1] + 1 else countLong[1], 0);

def countShort = CompoundValue(1, if IsNaN(countShort[1])  or BarNumber() == 1 then 0 else if isOrder and isShort[1] then countShort[1] + 1 else countShort[1], 0);

# What was the biggest winning and losing trade
def biggestWin = CompoundValue(1, if IsNaN(biggestWin[1]) or BarNumber() == 1 then 0 else if isOrder and (dollarProfitLoss > 0) and (dollarProfitLoss > biggestWin[1]) then dollarProfitLoss else biggestWin[1], 0);

def biggestLoss = CompoundValue(1, if IsNaN(biggestLoss[1]) or BarNumber() == 1 then 0 else if isOrder and (dollarProfitLoss < 0) and (dollarProfitLoss < biggestLoss[1]) then dollarProfitLoss else biggestLoss[1], 0);

# What percent were winners
def PCTWin = Round((profitWinners / orderCount) * 100, 2);
def PCTLongWin = Round((profitLongWinners / countLong) * 100, 2);
def PCTShortWin = Round((profitShortWinners / countShort) * 100, 2);

# Average trade
def avgTrade = Round((dollarPLSum / orderCount), 2);


#######################################
##  Create Labels
#######################################

#AddLabel(showLabels, GetSymbol()+" Tick Size: "+TickSize()+" Value: "+TickValue(), color.white, location = Location.BOTTOM_LEFT);
AddLabel(showLabels, "L-Orders: " + orderCount + " P/L: " + AsDollars(dollarPLSum), if dollarPLSum > 0 then Color.GREEN else if dollarPLSum < 0 then Color.RED else Color.GRAY, location = Location.BOTTOM_LEFT);
#AddLabel(if !IsNan(orderPrice) and showLabels then 1 else 0, "Closed+Open P/L: "+ AsDollars(TradePL+dollarPLSum), if ((TradePL+dollarPLSum) > 0) then color.green else if ((TradePL+dollarPLSum) < 0) then color.red else color.gray, location = Location.BOTTOM_LEFT);

#AddLabel(showLabels, "Avg per Trade: "+ AsDollars(avgTrade), if avgTrade > 0 then Color.Green else if avgTrade < 0 then Color.RED else Color.GRAY, location = Location.BOTTOM_LEFT);

AddLabel(showLabels, "Winners: " + PCTWin + "%", if PCTWin > 50 then Color.GREEN else if PCTWin > 40 then Color.YELLOW else Color.GRAY, location = Location.BOTTOM_LEFT);
AddLabel(showLabels, "Long Win: " + PCTLongWin + "%", if PCTLongWin > 50 then Color.GREEN else if PCTLongWin > 40 then Color.YELLOW else Color.GRAY, location = Location.BOTTOM_LEFT);
AddLabel(showLabels, "Short Win: " + PCTShortWin + "%", if PCTShortWin > 50 then Color.GREEN else if PCTShortWin > 40 then Color.YELLOW else Color.GRAY, location = Location.BOTTOM_LEFT);

#AddLabel(showLabels, "MaxUp: "+ AsDollars(biggestWin) +" MaxDown: "+AsDollars(biggestLoss), color.white, location = Location.BOTTOM_LEFT);
AddLabel(showLabels, "Long Profit: " + AsDollars(profitLong), if profitLong > 0 then Color.GREEN else if profitLong < 0 then Color.RED else Color.GRAY, location = Location.BOTTOM_LEFT);
AddLabel(showLabels, "Short Profit: " + AsDollars(profitShort), if profitShort > 0 then Color.GREEN else if profitShort < 0 then Color.RED else Color.GRAY, location = Location.BOTTOM_LEFT);
#AddLabel(if !IsNan(CurrentPosition) and showLabels then 1 else 0, "Open: "+ (If isLong then "Bought" else "Sold") + " @ "+orderPrice, color.white, location = Location.BOTTOM_LEFT);
#AddLabel(if !IsNan(orderPrice) and showLabels then 1 else 0, "Open Trade P/L: "+ AsDollars(TradePL), if (TradePL > 0) then color.green else if (TradePl < 0) then color.red else color.gray, location = Location.BOTTOM_LEFT);



#######################################
##  Chart Bubbles for Profit/Loss
#######################################


AddChartBubble(showSignals and showBubbles and isOrder and isLong[1], low, "$" + dollarProfitLoss, if dollarProfitLoss == 0 then Color.LIGHT_GRAY else if dollarProfitLoss > 0 then Color.GREEN else Color.RED, 0);
AddChartBubble(showSignals and showBubbles and isOrder and isShort[1], high,  "$" + dollarProfitLoss, if dollarProfitLoss == 0 then Color.LIGHT_GRAY else if dollarProfitLoss > 0 then Color.GREEN else Color.RED, 1);

#AssignPriceColor(if CurrentPosition == 1 then color.green else if CurrentPosition == -1 then color.red else color.gray);


#-- END of CODE
 
Last edited:
I'm a big fan of ZigZag indicators. A general problem is they are repainters.

A notable exception is the QQE ZigZag, an indicator that I find very useful.
https://usethinkscript.com/threads/...ncl-qqe-non-repainting-for-thinkorswim.12546/

Laguerre filters are fast-acting methods to smooth inputs. John Eulers described a fast Laguerre method to smooth financial market data in his article entitled "Time Warp - Without Space Travel". Speed is good; speed with market data is fast money.
https://www.mesasoftware.com/papers/TimeWarp.pdf

The below code uses a Eulers High-Low Laguerre Filter (EHLLF) to find uptrends and downtrends, creating a responsive, non-repainting ZigZag.

The plots include the LaguerreZigZag, along with Eulers' Filter and Finite ImpulseResponse (FIR).

All plots except the LaguerreZigZag work in TOS Mobile apps (due to the EnableApproximation function). Use the UpSignal and DownSignal with Boolean value, up or down arrows to see where the Zig Zags.

The image shows the QQE ZigZag on the left and the LaguerreZigZag on the right. Both for SPX, 5 minute time frame for the last 5 days.

View attachment 24466

Enjoy!

Code:
# Laguerre ZigZag - A responsive, non-repainting ZigZag indicator
# Authored by whoDAT - April 1, 2025

# A HL2 input is typically better to reduce the number of zigs and zags.
# The Gamma is the dampening function.
#  - Increasing Gamma values (approaching 1.0) will dampen the input more, reducing the number of zigs and zags.
#  - Lower Gamma values (approaching 0.0) will make a noisy output with more zigs and zags.
# The LowHighLookBack defines the how far back to look for the lowest or highest value to use when painting the ZigZag line.

input Price = HL2;
input Gamma = 0.80;
input LowHighLookback = 10;

# Laguerre filter
def L0 = (1 - Gamma) * Price + (Gamma * L0[1]);
def L1 = (-1 * Gamma * L0) + L0[1] + (Gamma * L1[1]);
def L2 = (-1 * Gamma * L1) + L1[1] + (Gamma * L2[1]);
def L3 = (-1 * Gamma * L2) + L2[1] + (Gamma * L3[1]);

def Filt = (L0 + (2 * L1) + (2 * L2) + L3) / 6;
def FIR = (Price + (2 * Price[1]) + (2 * Price[2]) + Price[3]) / 6;

# If the Laguerre filter is increasing (or decreasing), this is an uptick (downtick). That is used to make the upward or downward signal to find the zigs and zags.
def Uptick = if (L0 > Filt) then 1 else Double.NaN;
def Downtick = if (L0 < Filt) then 1 else Double.NaN;

def UpSig = if Downtick[1] == 1 and Uptick[0] == 1 then Lowest(low, LowHighLookback) else Double.NaN;
def DnSig = if Uptick[1] == 1 and Downtick[0] == 1 then Highest(high, LowHighLookback) else Double.NaN;

def ZZL = if !IsNaN(UpSig) then UpSig else if !IsNaN(DnSig)  then DnSig else Double.NaN;

plot ZigZagLaguerre = ZZL;
ZigZagLaguerre.EnableApproximation();

plot FilterResponse = Filt;
plot FiniteImpulseResponse = FIR;
plot UpSignal = UpSig;
plot DownSignal = DnSig;

# END OF CODE
@whoDAT Do you have any idea why the up/down signals don't plot?

I am looking for a zigzag function that I can use in a custom watchlist column to provide a "heads up" that trend is shifting. I tried using criksdds AGAIG No Arrows but it is a repainter and does not work in a watchlist.

The goal is to change the background in the watchlist column based on trend direction and I think that if I can figure out why the up/down signals are not showing, I'll be able to capture the info.

Screenshot 2026-02-19 104509.jpg


Thanks.
 
Last edited:
@whoDAT Do you have any idea why the up/down signals don't plot?

I am looking for a zigzag function that I can use in a custom watchlist column to provide a "heads up" that trend is shifting. I tried using criksdds AGAIG No Arrows but it is a repainter and does not work in a watchlist.

The goal is to change the background in the watchlist column based on trend direction and I think that if I can figure out why the up/down signals are not showing, I'll be able to capture the info.

View attachment 27075

Thanks.
If you are looking for a "heads up" a zig zag is not going to do that, not even a non-repaint hybrid version, if fact a true non-repainting ZigZag will always lag more than the repainting version. Using zig zags is like reading the wall street journal about yesterdays news.
What exactly are you looking for - a heads up on what exactly? There are better tools.
 
This indicator may be what you are looking for
Code:
declare lower;

# ===== INPUTS =====
input fastLength = 9;
input slowLength = 21;
input atrLength = 14;
input expansionLevel = 0.5;      # ATR units where expansion begins
input stretchLevel = 1.0;        # ATR units where trend is stretched

# ===== EMAs =====
def fastEMA = ExpAverage(close, fastLength);
def slowEMA = ExpAverage(close, slowLength);

# ===== ATR =====
def atr = Average(TrueRange(high, close, low), atrLength);

# ===== EMA DISTANCE =====
def emaDistance = fastEMA - slowEMA;
def distanceATR = emaDistance / atr;

# ===== PLOT DISTANCE =====
plot SpreadATR = distanceATR;
SpreadATR.SetLineWeight(2);

# ===== ZERO LINE =====
plot ZeroLine = 0;
ZeroLine.SetDefaultColor(Color.GRAY);

# ===== COLOR LOGIC =====
SpreadATR.AssignValueColor(
    if distanceATR > stretchLevel then Color.GREEN
    else if distanceATR > expansionLevel then Color.DARK_GREEN
    else if distanceATR < -stretchLevel then Color.RED
    else if distanceATR < -expansionLevel then Color.DARK_RED
    else Color.YELLOW
);

# ===== TREND EXPANSION STATES =====
def bullishExpansion = distanceATR > expansionLevel;
def bullishStretch = distanceATR > stretchLevel;
def bearishExpansion = distanceATR < -expansionLevel;
def bearishStretch = distanceATR < -stretchLevel;
def compression = AbsValue(distanceATR) < expansionLevel;

# ===== TREND METER LABEL =====
AddLabel(yes,
    if bullishStretch then "TREND: BULL STRETCHED"
    else if bullishExpansion then "TREND: BULL EXPANDING"
    else if bearishStretch then "TREND: BEAR STRETCHED"
    else if bearishExpansion then "TREND: BEAR EXPANDING"
    else "TREND: COMPRESSION",
    if bullishStretch then Color.GREEN
    else if bullishExpansion then Color.DARK_GREEN
    else if bearishStretch then Color.RED
    else if bearishExpansion then Color.DARK_RED
    else Color.YELLOW
);

How To Read It

Yellow Zone​

Compression
Market coiling
Breakout likely

Dark Green / Dark Red​

Expansion beginning
Momentum building
Best entry phase

Bright Green / Bright Red

Trend stretched
Late stage
Good area for:
• Taking profits
• Selling premium
• Watching for pullback

I like to use EMA 5 and 9 for intraday stuff and 9 and 21 for swings
 
Watchlist Column
Code:
input fastLength = 9;
input slowLength = 21;
input atrLength = 14;
input expansionLevel = 0.5;
input stretchLevel = 1.0;

def fastEMA = ExpAverage(close, fastLength);
def slowEMA = ExpAverage(close, slowLength);
def atr = Average(TrueRange(high, close, low), atrLength);

def spreadATR = (fastEMA - slowEMA) / atr;

AssignBackgroundColor(
    if spreadATR > stretchLevel then Color.GREEN
    else if spreadATR > expansionLevel then Color.DARK_GREEN
    else if spreadATR < -stretchLevel then Color.RED
    else if spreadATR < -expansionLevel then Color.DARK_RED
    else Color.GRAY
);

AddLabel(yes,
    if spreadATR > stretchLevel then "BULL+"
    else if spreadATR > expansionLevel then "BULL"
    else if spreadATR < -stretchLevel then "BEAR+"
    else if spreadATR < -expansionLevel then "BEAR"
    else "FLAT",
    Color.WHITE);
 
@antwerks Thanks for responding. Much of this work was inspired by you.

My goal is to have an analysis window where I can look at multiple indicators and make trading decisions. To make it easy to choose stocks that are potentially in a buy scenario, I am using a scan from BenTen I think: http://tos.mx/!wCAHglnJ

I then take the scan results and put them in a watchlist. The goal is to have a custom column in the watchlist that visually (by changing background colors) the stocks that are in a potential buy scenario. Since it is in a watchlist and that is linked to my analysis window, I can quickly look at potential stocks to determine if they are really in a tactical buy scenario.

To that end, I built (and continue to refine) a non-plotting summary of all of the key points of inflection of all of the indicators that I am using.

The first attempt was to put them into a 4x4 flexible grid. However, this was a PITA to have to manually update the stocks in each window and then have to manually select that stock for the analysis window.

Analysis window:
Analysis Window.jpg


Grid window:
Grid.jpg


To avoid the cumbersomeness of this approach, I took the code and tweaked it for insertion into a custom watchlist column just changing the background if the summary of the key indicator metrics crossed a threshold:
Watchlist.jpg


As I used the custom column in the analysis window, I realized that a lot of the time criksdds's AGAIG NoArrows indicator flagged the beginning of the trend earlier than my summary. After much blood, sweat and tears I realized that ZigZagHighLow was a repainter and could never be used in the watchlist custom column to catch and reflect the trend change shown by his Long/Short Bubbles:

Screenshot 2026-02-19 104509.jpg


His NoArrows code:

# AsGoodAsItGets Indicator without Arrows
#CSR Buy/Sell Arrows with Short/Long Bubbles
#Developed 4-9-22 First Edition 8-23-22 Revised
#No Arrow Edition 1/1/23

declare upper;

input atrreversal = 2.0;

def priceh = MovingAverage(AverageType.EXPONENTIAL, high, 5);
def pricel = MovingAverage(AverageType.EXPONENTIAL, low, 5);

def EIL = ZigZagHighLow("price h" = priceh, "price l" = pricel, "percentage reversal" = .01, "absolute reversal" = .05, "atr length" = 5, "atr reversal" = atrreversal).lastL;
def EIH = ZigZagHighLow("price h" = priceh, "price l" = pricel, "percentage reversal" = .01, "absolute reversal" = .05, "atr length" = 5, "atr reversal" = atrreversal).lastH;

plot signaldown = !isNAN(EIH);

AddChartBubble(SignalDown, high+.01, "Short", Color.RED, yes);

plot signalrevBot = !isNaN(EIL);

AddChartBubble(Signalrevbot, low-.01, "Long", Color.GREEN, no);


input usealerts = yes;
alert(usealerts and signaldown[1] == 1, "Short", alert.bar, sound.ring);
alert(usealerts and signalrevbot[1] == 1, "Long", alert.bar, sound.ring);


BenTen lead me to look at using @whoDAT Laguerre ZigZag since they are not repainters and can be evaluated during the current bar.

I haven't started writing the new watchlist column since I wasn't sure that I understood how to capture the trend reversal in his indicator.

I look forward to hearing your thoughts.
 
This indicator may be what you are looking for
Code:
declare lower;

# ===== INPUTS =====
input fastLength = 9;
input slowLength = 21;
input atrLength = 14;
input expansionLevel = 0.5;      # ATR units where expansion begins
input stretchLevel = 1.0;        # ATR units where trend is stretched

# ===== EMAs =====
def fastEMA = ExpAverage(close, fastLength);
def slowEMA = ExpAverage(close, slowLength);

# ===== ATR =====
def atr = Average(TrueRange(high, close, low), atrLength);

# ===== EMA DISTANCE =====
def emaDistance = fastEMA - slowEMA;
def distanceATR = emaDistance / atr;

# ===== PLOT DISTANCE =====
plot SpreadATR = distanceATR;
SpreadATR.SetLineWeight(2);

# ===== ZERO LINE =====
plot ZeroLine = 0;
ZeroLine.SetDefaultColor(Color.GRAY);

# ===== COLOR LOGIC =====
SpreadATR.AssignValueColor(
    if distanceATR > stretchLevel then Color.GREEN
    else if distanceATR > expansionLevel then Color.DARK_GREEN
    else if distanceATR < -stretchLevel then Color.RED
    else if distanceATR < -expansionLevel then Color.DARK_RED
    else Color.YELLOW
);

# ===== TREND EXPANSION STATES =====
def bullishExpansion = distanceATR > expansionLevel;
def bullishStretch = distanceATR > stretchLevel;
def bearishExpansion = distanceATR < -expansionLevel;
def bearishStretch = distanceATR < -stretchLevel;
def compression = AbsValue(distanceATR) < expansionLevel;

# ===== TREND METER LABEL =====
AddLabel(yes,
    if bullishStretch then "TREND: BULL STRETCHED"
    else if bullishExpansion then "TREND: BULL EXPANDING"
    else if bearishStretch then "TREND: BEAR STRETCHED"
    else if bearishExpansion then "TREND: BEAR EXPANDING"
    else "TREND: COMPRESSION",
    if bullishStretch then Color.GREEN
    else if bullishExpansion then Color.DARK_GREEN
    else if bearishStretch then Color.RED
    else if bearishExpansion then Color.DARK_RED
    else Color.YELLOW
);

How To Read It

Yellow Zone​

Compression
Market coiling
Breakout likely

Dark Green / Dark Red​

Expansion beginning
Momentum building
Best entry phase

Bright Green / Bright Red

Trend stretched
Late stage
Good area for:
• Taking profits
• Selling premium
• Watching for pullback

I like to use EMA 5 and 9 for intraday stuff and 9 and 21 for swings
Thanks for this, the scanner and watchlist. I'll test it this afternoon.
 
A little advice on the indicator and what you are seeing...What Your Indicator Is Actually Measuring

This script measures Distance between 9 EMA and 21 EMA, normalized by ATR

So it’s not measuring price directly, it’s measuring trend expansion strength.

Why Price Can Go Up While Spread Goes Down

In the chart:
  • Price continues making higher highs.
  • BUT the EMA spread is shrinking.
That means:
  • The 9 EMA is still above the 21 EMA (trend still bullish)
  • BUT the 9 EMA is moving closer to the 21 EMA
  • So the trend is still up…
  • But the rate of separation is slowing
Translation - That is loss of momentum, not necessarily trend reversal.
It means:
  • Buyers are still in control
  • But they are not pushing as aggressively
  • The move is becoming more grindy
  • Risk of pullback increases
What That Usually Leads To is When SpreadATR declines while price rises:
  1. Consolidation
  2. Shallow pullback
  3. Deeper pullback
  4. Or eventual rollover (if it crosses zero)
It is an early warning tool, not a reversal signal.

Think of It Like This
  • Expanding spread = acceleration
  • Flat spread = steady climb
  • Shrinking spread = deceleration
  • Negative spread = bearish control
You're seeing deceleration.

The Important Detail in the Image is to Notice:
  • Big expansion spike during breakout
  • Then steady decline while price grinds higher
That’s classic Impulse move then digestion phase. Very common in trending stocks like WULF.

How To Use This Properly

If:
Spread rising + price rising then aggressive continuation trades
Spread falling + price rising then tighten stops / avoid chasing
Spread below expansionLevel then compression leading to breakout coming

Advanced Insight

Your settings:
(intraday settings here)

expansionLevel = 0.3
stretchLevel = 0.8


These are actually pretty tight. If you want fewer “compression” readings:
  • Try expansionLevel = 0.4
  • stretchLevel = 1.0
It will filter noise.

Bottom Line

Yes — what you're seeing is Bullish trend with decreasing momentum ( in current image)

Not bearish.
Not broken.
Just cooling.

 

Similar threads

Not the exact question you're looking for?

Start a new thread and receive assistance from our community.

87k+ Posts
1028 Online
Create Post

Similar threads

Similar threads

The Market Trading Game Changer

Join 2,500+ subscribers inside the useThinkScript VIP Membership Club
  • Exclusive indicators
  • Proven strategies & setups
  • Private Discord community
  • ‘Buy The Dip’ signal alerts
  • Exclusive members-only content
  • Add-ons and resources
  • 1 full year of unlimited support

Frequently Asked Questions

What is useThinkScript?

useThinkScript is the #1 community of stock market investors using indicators and other tools to power their trading strategies. Traders of all skill levels use our forums to learn about scripting and indicators, help each other, and discover new ways to gain an edge in the markets.

How do I get started?

We get it. Our forum can be intimidating, if not overwhelming. With thousands of topics, tens of thousands of posts, our community has created an incredibly deep knowledge base for stock traders. No one can ever exhaust every resource provided on our site.

If you are new, or just looking for guidance, here are some helpful links to get you started.

What are the benefits of VIP Membership?
VIP members get exclusive access to these proven and tested premium indicators: Buy the Dip, Advanced Market Moves 2.0, Take Profit, and Volatility Trading Range. In addition, VIP members get access to over 50 VIP-only custom indicators, add-ons, and strategies, private VIP-only forums, private Discord channel to discuss trades and strategies in real-time, customer support, trade alerts, and much more. Learn all about VIP membership here.
How can I access the premium indicators?
To access the premium indicators, which are plug and play ready, sign up for VIP membership here.
Back
Top