Create your own
Lesson illustration

Creating Non-Repainting Signals with Markers and Alerts

Welcome back. In the previous lesson, you built a configurable MACD indicator and separated a Pine script into inputs, calculations, and visual output. We also established the key fact behind reliable signal design: values based on an open candle can still change.

Today, you will turn that foundation into a confirmed EMA-crossover signal indicator. It will calculate a rule on every bar, mark confirmed bullish and bearish events directly on the price chart, and expose two alert conditions in TradingView. The aim is not to create a profitable trading system from one crossover; it is to create a signal whose displayed historical behavior matches what the script could have known at bar close.


1. What “non-repainting” means for this indicator

A signal is often called non-repainting when a marker that was valid at a bar’s close does not later disappear or move because the script used provisional information from that same bar.

That description needs precision. On a live bar:

  • open is fixed from the start of the bar.
  • high, low, and close are fluid until the bar closes.
  • An EMA calculated from close can therefore move during the bar.
  • A crossover may briefly appear, then vanish before the close.

If a script signals immediately on that temporary crossover, you might see a bullish marker intrabar that disappears later. Historical charts only show the final, closed result, so the past would look cleaner than real-time use actually was.

For a signal intended for bar-close decisions, we will require both:

  1. An event condition, such as a fast EMA crossing a slow EMA.
  2. barstate.isconfirmed, meaning the bar has closed and its OHLC values are final.

The official TradingView documentation is worth reading here because “repainting” is broader than a simple good/bad label. It distinguishes normal intrabar fluctuation from genuinely misleading chart behavior.

Concepts / Repainting

Read TradingView’s official explanation of repainting to understand why a bar-close confirmation guard is necessary, and what it does and does not guarantee.

Begin with the “Introduction” and then read “Historical vs realtime calculations” > “Fluid data values.” Focus on fluid price explanation: historical bars contain final OHLC values, whereas live bars update repeatedly. Then continue to the passage beginning “To prevent this repainting” and note the use of barstate.isconfirmed. Finally, in “Bar state built-ins,” read the explanation that confirmed bars reproduce consistently in historical calculations.

A precise claim

Our completed script makes the following limited but useful claim:

On the chart’s own timeframe, using only its own price data, a displayed signal is produced only when the crossover exists at the close of that bar.

It does not mean:

  • every future script extension will be non-repainting;
  • the indicator forecasts price direction reliably;
  • you could have filled an order at the exact closing price that generated the signal;
  • a signal is automatically a trade entry.

A decision based on the closing value is available only at the close; any real order would generally occur later and can experience spread, slippage, or a gap. That distinction will matter when you turn rules into paper-trading plans.


2. The rule: a confirmed EMA crossover

We will use two exponential moving averages on the chart:

  • a fast EMA, responsive to recent price;
  • a slow EMA, smoother and slower to react.

A bullish crossover happens when the fast EMA moves from at or below the slow EMA to above it. A bearish crossunder is the reverse. Pine provides these event tests:

ta.crossover(fastEma, slowEma)
ta.crossunder(fastEma, slowEma)

Unlike a condition such as fastEma > slowEma, a crossover is normally true on the transition bar only. This is important for alert design:

ConditionCan remain true for many bars?Typical use
fastEma > slowEmaYesTrend state or filter
ta.crossover(fastEma, slowEma)No, normally one barEvent marker or alert
ta.crossover(...) and barstate.isconfirmedNo, and only at bar closeConfirmed event marker or alert

The confirmation guard is separate from the crossover logic. The crossover identifies what happened; barstate.isconfirmed determines when you accept it as a signal.


3. Build the confirmed-signal indicator

Open a liquid instrument in TradingView. For visual testing, a daily US or UK share, BTCUSD, or a liquid commodity product is suitable. A shorter timeframe will generate more markers, but it will also expose you to more market noise, so do not mistake marker frequency for quality.

In the Pine Editor, create a new indicator and replace its contents with the following code:

//@version=6
indicator("Confirmed EMA Crossover Alerts", shorttitle = "EMA Confirm", overlay = true)

// Inputs
fastLengthInput = input.int(20, "Fast EMA length", minval = 1, group = "Calculation")
slowLengthInput = input.int(50, "Slow EMA length", minval = 1, group = "Calculation")
showEmaInput = input.bool(true, "Show EMA lines", group = "Display")

// Calculations: run on every bar
fastEma = ta.ema(close, fastLengthInput)
slowEma = ta.ema(close, slowLengthInput)

bullishCross = ta.crossover(fastEma, slowEma)
bearishCross = ta.crossunder(fastEma, slowEma)

// Only accept crossover events after the bar has closed
bullishSignal = bullishCross and barstate.isconfirmed
bearishSignal = bearishCross and barstate.isconfirmed

// Visual context
plot(
     showEmaInput ? fastEma : na,
     title = "Fast EMA",
     color = color.teal,
     linewidth = 2
)

plot(
     showEmaInput ? slowEma : na,
     title = "Slow EMA",
     color = color.orange,
     linewidth = 2
)

// Confirmed event markers
plotshape(
     bullishSignal,
     title = "Confirmed bullish signal",
     style = shape.labelup,
     location = location.belowbar,
     color = color.lime,
     text = "BULL",
     textcolor = color.white,
     size = size.tiny
)

plotshape(
     bearishSignal,
     title = "Confirmed bearish signal",
     style = shape.labeldown,
     location = location.abovebar,
     color = color.red,
     text = "BEAR",
     textcolor = color.white,
     size = size.tiny
)

// Alert definitions
alertcondition(
     bullishSignal,
     title = "Confirmed bullish EMA crossover",
     message = "Confirmed bullish EMA crossover on {{ticker}} ({{interval}}). Close: {{close}}"
)

alertcondition(
     bearishSignal,
     title = "Confirmed bearish EMA crossunder",
     message = "Confirmed bearish EMA crossunder on {{ticker}} ({{interval}}). Close: {{close}}"
)

Save the script, then select Add to chart.

Read the structure as four layers

The script is deliberately small, but it has four distinct responsibilities.

1. Inputs

The EMA lengths are configurable rather than hard-coded. The defaults, and , are conventional demonstration values, not recommendations.

Keep the slow length larger than the fast length. If the fast EMA has a length of and the slow EMA a length of , the names correctly describe their behavior. Reversing them would invert the intended meaning of bullish and bearish crossover conditions.

2. Calculations

fastEma = ta.ema(close, fastLengthInput)
slowEma = ta.ema(close, slowLengthInput)

These calculations run on every historical bar and every update of the live bar. That is the correct Pine pattern: calculate consistently first, then decide how and when to use the result.

3. Confirmed event logic

bullishSignal = bullishCross and barstate.isconfirmed

On a historical bar, barstate.isconfirmed is true because that bar has already closed. On the live bar, it becomes true only at the final update when the bar closes.

This means that a crossover which occurs and reverses within an open bar does not produce a marker or satisfy the alert condition. The cost of that reliability is delay: the signal cannot be known until confirmation.

4. Outputs and alert declarations

plotshape() gives the rule a visible chart representation. The shape appears below a bullish signal bar and above a bearish signal bar. alertcondition() does not send an alert by itself; it makes a named condition available when you create an alert through TradingView’s interface.

The official documentation’s “Text and shapes” page is a useful compact reference for the visual part of the script.

Visuals / Text and shapes

Read the official plotshape() reference to understand why a boolean signal can be rendered as a chart label without creating and managing label objects manually.

In the “plotshape()” section, read the plotshape overview, then inspect the example that uses shape.arrowup. Focus on the roles of series, style, location, color, and text. The table of available shapes is reference material; you do not need to memorize it.

For a quick visual walkthrough of the same mechanism, watch the marker and alert segments from The Trading Parrot’s tutorial.

Pine Script Tutorial: Complete Beginner's Guide to TradingView

In “Pine Script Tutorial: Complete Beginner's Guide to TradingView” by The Trading Parrot, the presenter demonstrates how a boolean crossover condition becomes a label on the price chart and then becomes an alert option.

Watch marker setup to see plotshape() configured with labels, locations, colors, and text. Then watch alert setup for the connection between alertcondition() declarations and TradingView’s Create Alert dialog. Apply the workflow to the confirmed conditions in your own script, not the tutorial’s unconfirmed crossover example.


4. Configure the actual TradingView alerts

After adding the indicator, create the alerts separately:

  1. Select the Alert button on the TradingView chart.
  2. Under Condition, select Confirmed EMA Crossover Alerts.
  3. Select Confirmed bullish EMA crossover.
  4. Set the frequency to Once Per Bar Close.
  5. Choose an appropriate notification method, such as an app notification or email.
  6. Create a separate alert for Confirmed bearish EMA crossunder, also using Once Per Bar Close.

The frequency setting is a second layer of operational protection. The code already rejects unconfirmed bars through barstate.isconfirmed; the alert frequency ensures TradingView does not try to notify you repeatedly while a bar is forming.

A TradingView EUR/USD chart with bullish and bearish alert markers, illustrating how chart markers make alert events inspectable rather than leaving them as invisible notifications.

Two operational details are easy to overlook:

  • Alerts trigger only going forward. Historical BULL and BEAR labels prove how the rule would have evaluated on completed bars, but creating an alert does not generate notifications for old labels.
  • An alert uses the script and settings present when you create it. If you materially change the code or its input settings, delete and recreate the alert so its live behavior matches the chart version you are inspecting.

Why use event conditions rather than trend states?

Suppose you wrote this:

bullishSignal = fastEma > slowEma and barstate.isconfirmed

Once the fast EMA rises above the slow EMA, this condition could be true at the close of every subsequent bar for days or weeks. With an alert set to once per bar close, it could send one alert per bar.

Our version uses ta.crossover(), which identifies the transition rather than the continuing state. One confirmed event yields one marker and one candidate alert. If you later use persistent conditions intentionally, you will need a separate state-management rule to avoid repeated alerts.


5. Validate the signal before trusting its appearance

Treat your script like a small production component: validate the behavior you specified, rather than accepting a visually appealing chart as evidence.

Inspect the historical chart

Scroll through several different markets and timeframes. Confirm that:

  • BULL labels occur on bars where the fast EMA has crossed above the slow EMA.
  • BEAR labels occur on bars where the fast EMA has crossed below the slow EMA.
  • labels do not appear repeatedly while one EMA simply remains above the other;
  • hiding EMA lines changes only display, not marker logic.

Use TradingView’s Data Window to inspect the fast and slow EMA values on a marker bar. The visual label should match the numeric relationship.

Observe a live bar when practical

On a liquid market and a shorter timeframe, watch the current bar near a potential EMA crossover. The EMA lines can move around during the bar, but a BULL or BEAR marker should not become final until the candle closes.

A marker appearing at the close is not “late” because the script failed. The close is explicitly the information boundary you chose.

Change parameters without curve-fitting

Try a smaller pair such as and , then restore and . Smaller lengths typically create more frequent crosses and more whipsaws. Larger lengths produce fewer, slower events.

Do not search dozens of settings until historical labels look ideal. That is the beginning of overfitting: adapting a rule to old price noise rather than testing whether it is robust across assets and future periods.


6. Keep the non-repainting scope intact

Our script is simple partly because it uses only data from the current chart timeframe. That is intentional.

A common next step is to request a higher-timeframe trend filter, such as a daily EMA while viewing a one-hour chart. That can be useful, but request.security() introduces another repainting risk: a higher-timeframe candle is itself incomplete until it closes.

A Bitcoin chart comparing a fluctuating higher-timeframe value with a confirmed higher-timeframe value; the changing line demonstrates why unconfirmed data from another timeframe can make past and live behavior differ.

For now, preserve these boundaries:

  • Do not add request.security() calls until you can deliberately request confirmed higher-timeframe values.
  • Do not use positive plot offsets to make a marker look earlier or better positioned.
  • Do not use future-dependent constructs, such as pivot logic, as if a pivot were known on the pivot bar itself.
  • Keep barstate.isconfirmed attached to any signal intended for close-based alerts.

The next lesson will convert indicator logic into a basic Pine strategy with entries and exits, then interpret Strategy Tester results. A non-repainting indicator is the right starting point, but a strategy must also model orders, costs, timing, and the difference between a chart signal and an executable trade.


Key takeaways

A dependable bar-close signal has four parts:

  • a clearly defined event rule, here an EMA crossover or crossunder;
  • confirmation with barstate.isconfirmed;
  • visible markers driven by the same confirmed boolean conditions;
  • alertcondition() declarations that expose those conditions in TradingView’s alert dialog.

plotshape() is only visual output, while alertcondition() only defines an alert choice. The actual live notification is created separately in TradingView and should be configured Once Per Bar Close.

Most importantly, non-repainting does not mean predictive or profitable. It means the marker represents information that was available when the bar closed, rather than a favorable-looking result created from intrabar or future information.

Can't find a good explanation? Sign up and we'll make it for you

Sign up