Edgoro
Indicator · Markers, drawings and colors

Pine Script plotshape: buy and sell signal markers

Draw buy and sell markers in Pine Script v6 with plotshape() and plotchar(): a complete script, the fixed-text rule, positions, sizes and alerts.

Short answer

Call plotshape() at the top level of the script with a bool condition as its first argument: TradingView draws the shape on every bar where the condition is true. Put buy markers under the bar with location.belowbar and sell markers above it with location.abovebar.

The script

An EMA cross filtered by RSI: labelled markers for the signals the filter keeps, small dots for the crosses it rejects, and alerts for both sides. Paste it into a new indicator in the Pine Editor, then save and add it to the chart.

Pine Script v6
//@version=6
indicator("Buy and sell markers", overlay = true)

fastLen = input.int(9, "Fast EMA", minval = 1)
slowLen = input.int(21, "Slow EMA", minval = 2)
rsiLen  = input.int(14, "RSI length", minval = 2)

fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)
rsi  = ta.rsi(close, rsiLen)

crossUp   = ta.crossover(fast, slow)
crossDown = ta.crossunder(fast, slow)

// Signals the RSI filter keeps, only once the bar has closed
buy  = crossUp and rsi < 70 and barstate.isconfirmed
sell = crossDown and rsi > 30 and barstate.isconfirmed
// Crosses the filter rejects
skipBuy  = crossUp and rsi >= 70 and barstate.isconfirmed
skipSell = crossDown and rsi <= 30 and barstate.isconfirmed

plot(fast, "Fast EMA", color.teal)
plot(slow, "Slow EMA", color.orange)

// Labels with text: the text must be a fixed string
plotshape(buy, "Buy", shape.labelup, location.belowbar, color.teal, text = "BUY", textcolor = color.white, size = size.small)
plotshape(sell, "Sell", shape.labeldown, location.abovebar, color.red, text = "SELL", textcolor = color.white, size = size.small)

// One character per bar, no text
plotchar(skipBuy, "Rejected buy", "•", location.belowbar, color.gray, size = size.tiny)
plotchar(skipSell, "Rejected sell", "•", location.abovebar, color.gray, size = size.tiny)

alertcondition(buy, "Buy", "Buy signal on {{ticker}} at {{close}}")
alertcondition(sell, "Sell", "Sell signal on {{ticker}} at {{close}}")

How it works

  • plotshape(buy, "Buy", shape.labelup, location.belowbar, color.teal, ...). The first argument is the condition: where it's true the shape is drawn, where it's false nothing is. The next four positional arguments are the title (the name in the Style tab, where each marker can be hidden), the shape, the position and the color. Everything after that is clearer by name.
  • Shapes. shape.labelup and shape.labeldown are the only ones that hold text comfortably; the others are triangleup, triangledown, arrowup, arrowdown, circle, cross, xcross, diamond, square and flag. size goes from size.tiny to size.huge, or size.auto.
  • Positions. location.belowbar and location.abovebar follow each bar's low and high, location.top and location.bottom stick to the edges of the pane, and location.absolute puts the shape at the value of the series itself (see the variations).
  • plotchar() draws one character instead of a shape. Any single Unicode character works, which is handy for discreet marks like the dots here.
  • barstate.isconfirmed is part of each signal, so a marker only appears once its bar has closed and never vanishes afterwards. On historical bars it's always true, so the history is unchanged. The barstate.isconfirmed example shows what happens without it.
  • alertcondition() makes both signals available in TradingView's alert dialog; {{ticker}} and {{close}} are filled in when the alert fires. The differences with alert() are in alertcondition() vs alert().

Pitfalls

  • Text built from data. The text of plotshape() must be a fixed string written in the code. text = str.tostring(close) stops the script with series string used but const string expected. To print a price on a marker, draw a label with label.new(), as in the labels and lines example.
  • plotshape() inside an if block. if buy followed by an indented plotshape(...) gives Cannot use plotshape in local scope. Keep every plot*() call at the top level and pass the condition as the series.
  • abovebar and belowbar in a separate pane. With overlay = false, these two positions still follow the chart's bars, which stretches the pane's scale. In a pane, use location.top, location.bottom, or location.absolute with a value on the pane's own scale, such as rsi.
  • Markers that appear and then vanish. Without barstate.isconfirmed, a cross that exists for a few ticks of the live bar draws a marker, fires an alert, and is gone when the bar closes. See repainting explained.
  • Plot limit. Each plotshape() and plotchar() call counts toward the script's limit of 64 plots, like plot(). When a script has many signal types, use one call with a conditional color rather than one call per color.

Variations

Only the first signal of each side, at an exact price, with arrows. A var variable remembers the side of the last marker, so repeated buys in a row are skipped. location.absolute puts each triangle half an ATR beyond the bar, and plotarrow() draws an up arrow for positive values and a down arrow for negative ones.

//@version=6
indicator("First signal only", overlay = true)
fast = ta.ema(close, 9)
slow = ta.ema(close, 21)
atr  = ta.atr(14)
buy  = ta.crossover(fast, slow) and barstate.isconfirmed
sell = ta.crossunder(fast, slow) and barstate.isconfirmed

// Side of the last marker: 1 after a buy, -1 after a sell
var int side = 0
firstBuy  = buy and side != 1
firstSell = sell and side != -1
if firstBuy
    side := 1
if firstSell
    side := -1

// With location.absolute the series is the price where the shape goes
plotshape(firstBuy ? low - 0.5 * atr : na, "Buy", shape.triangleup, location.absolute, color.teal, size = size.small)
plotshape(firstSell ? high + 0.5 * atr : na, "Sell", shape.triangledown, location.absolute, color.red, size = size.small)
// Positive values draw an up arrow, negative values a down arrow
plotarrow(firstBuy ? 1 : firstSell ? -1 : na, "Arrows", color.teal, color.red)

Questions

How do I show the price on a plotshape marker?

You can't: plotshape() only accepts fixed text. Draw a label instead, for example label.new(bar_index, low, str.tostring(close, format.mintick), style = label.style_label_up). Labels accept text built from data, up to 500 per script with max_labels_count.

What is the difference between plotshape, plotchar and plotarrow?

plotshape() draws a built-in shape (triangle, label, circle, flag...) and can carry fixed text. plotchar() draws any single character. plotarrow() draws an up arrow for positive values and a down arrow for negative ones, with a length that grows with the value.

Why doesn't my plotshape appear on the chart?

Usually the condition is never true (plot it in the Data Window to check), the shape is hidden in the Style tab, or location.absolute is used with a bool: with absolute, the series must be the price where the shape goes, and na where there is no shape.

Every script on this page was checked with Edgoro Pine Check (Pine Script v6 rules). Pine Check is a static checker, not TradingView's compiler: TradingView compiles the script when you save it in the Pine Editor. The scripts are examples to learn from, not trading systems. Trading involves substantial risk of loss and is not suitable for every investor. Edgoro is a software tool, not a broker or financial adviser: nothing it produces is investment advice, and past or backtested performance does not guarantee future results.

Need a different version of this script?

Describe the change in plain English and Edgoro writes the Pine Script v6 for you, checked with Pine Check. For example:

“Mark a buy when the 9 EMA crosses above the 21 EMA with RSI under 70, a sell on the opposite cross, only the first signal of each side, with alerts.”