Pine Script v6 label.new() Complete Guide: Add Text Labels for Signals and Debugging

When building indicators or strategies in Pine Script v6, visualizing discrete events — such as buy/sell signals, pivot points, or debug values — directly on the chart is essential for both analysis and development. The label.new() function provides a powerful mechanism to place fully customizable text annotations at any bar and price level, making it the go-to tool for chart-based communication.

1. What Is label.new()?

label.new() creates a label drawing object anchored to a specific bar index and price (or y-axis) position. Unlike plotchar() or plotshape(), labels support arbitrary multi-line text, dynamic tooltip content, and a wide variety of visual styles. Every call to label.new() on a given bar creates one label object. For the complete parameter reference, see the official documentation: label.new() — Pine Script v6 Reference Manual.

2. Object Lifecycle and Limits

Label objects in Pine Script v6 are subject to a garbage collection limit. By default, only the most recent 50 labels are retained on the chart. The maximum can be raised to 500 by passing max_labels_count = 500 to indicator() or strategy(). When the limit is exceeded, the oldest labels are automatically deleted.

3. Core Parameters Explained

The following parameters are individually verified against the v6 Reference Manual. Do not treat this list as a complete function signature — always consult the official docs for the full parameter set.

Parameter Accepted Type Description
x series int Bar index (or UNIX timestamp when xloc = xloc.bar_time) where the label is anchored.
y series float Price level for the label anchor. Ignored when yloc is set to yloc.abovebar or yloc.belowbar.
text series string The text content displayed inside the label. Supports newline character \n.
xloc xloc Determines whether x is interpreted as a bar index (xloc.bar_index) or UNIX time (xloc.bar_time).
yloc yloc yloc.price uses the y value; yloc.abovebar / yloc.belowbar positions relative to the bar's high/low.
color series color Background fill color of the label bubble.
textcolor series color Color of the text inside the label.
style label_style Visual shape of the label (e.g., label.style_label_up, label.style_label_down, label.style_circle). Accepts only designated label.style_* constants.
size label_size Controls label size. Accepts size.tiny, size.small, size.normal, size.large, size.huge, size.auto.
tooltip series string Text shown when hovering over the label. Useful for embedding debug data without cluttering the chart.
textalign text_align Horizontal alignment of text within the label. Accepts text.align_left, text.align_center, text.align_right.

4. label.style_* Constants Reference

The style parameter accepts only label_style constants. The most commonly used ones are listed below. Never pass arbitrary strings to this parameter.

Constant Visual Shape
label.style_label_upBubble with pointer pointing upward (default for buy signals)
label.style_label_downBubble with pointer pointing downward (default for sell signals)
label.style_label_leftBubble with pointer pointing left
label.style_label_rightBubble with pointer pointing right
label.style_label_centerBubble with no pointer, centered
label.style_circleCircular shape
label.style_diamondDiamond shape
label.style_flagFlag shape
label.style_arrowupUpward arrow (no text bubble)
label.style_arrowdownDownward arrow (no text bubble)
label.style_xcrossX-cross marker
label.style_crossPlus-cross marker
label.style_squareSquare shape
label.style_triangleupUpward triangle
label.style_triangledownDownward triangle

5. Execution Model and the var Keyword

Because Pine Script executes bar-by-bar, calling label.new() unconditionally on every bar would create a new label on every single bar — quickly exhausting the 50-label default limit. The correct pattern is to call label.new() only inside a conditional block (e.g., when a signal fires). When you need to update an existing label rather than create a new one, store the returned label reference with var and use label.set_*() setter functions.

flowchart TD A[Bar Executes] --> B{Signal Condition True?} B -- No --> C[Skip label.new] B -- Yes --> D[Call label.new] D --> E{var label stored?} E -- No, new label --> F[Add to Label Pool] E -- Yes, update existing --> G[Call label.set_xy / label.set_text] G --> H[Same label object reused] F --> I{Pool size > max_labels_count?} I -- No --> J[Label visible on chart] I -- Yes --> K[Garbage Collector deletes oldest label] K --> J C --> L[Next Bar] J --> L H --> L

6. Pattern A — Signal Labels (Buy / Sell)

The most common use case: place a label below the bar on a bullish crossover and above the bar on a bearish crossover. Labels are created only when the condition is true, keeping object count minimal.

🔽 [Click to expand] View Full Pine Script Code — Signal Labels
//@version=6
indicator("Signal Labels Demo", overlay = true, max_labels_count = 200)

// ── Inputs ──────────────────────────────────────────────────────────────────
int   fastLen = input.int(9,  title = "Fast MA Length", minval = 1)
int   slowLen = input.int(21, title = "Slow MA Length", minval = 1)

// ── Moving Averages ──────────────────────────────────────────────────────────
float fastMA = ta.ema(close, fastLen)   // Fast exponential moving average
float slowMA = ta.ema(close, slowLen)   // Slow exponential moving average

// ── Crossover Conditions ─────────────────────────────────────────────────────
bool bullCross = ta.crossover(fastMA, slowMA)   // Fast crosses above slow → bullish
bool bearCross = ta.crossunder(fastMA, slowMA)  // Fast crosses below slow → bearish

// ── Plot Moving Averages ─────────────────────────────────────────────────────
plot(fastMA, title = "Fast EMA", color = color.new(color.blue, 0),  linewidth = 1)
plot(slowMA, title = "Slow EMA", color = color.new(color.orange, 0), linewidth = 1)

// ── Buy Label ────────────────────────────────────────────────────────────────
// Created only when bullCross is true; anchored below the bar's low.
if bullCross
    label.new(
         x         = bar_index,           // Current bar index
         y         = low,                 // Anchor at the bar's low price
         text      = "BUY",              // Label text
         yloc      = yloc.belowbar,       // Position below the bar automatically
         color     = color.new(color.green, 10),  // Green background
         textcolor = color.white,         // White text
         style     = label.style_label_up,        // Pointer pointing upward
         size      = size.small           // Small label size
    )

// ── Sell Label ───────────────────────────────────────────────────────────────
// Created only when bearCross is true; anchored above the bar's high.
if bearCross
    label.new(
         x         = bar_index,
         y         = high,
         text      = "SELL",
         yloc      = yloc.abovebar,       // Position above the bar automatically
         color     = color.new(color.red, 10),
         textcolor = color.white,
         style     = label.style_label_down,      // Pointer pointing downward
         size      = size.small
    )

7. Pattern B — Updating a Single Label (Debug / Live Value Display)

For debugging or displaying a live value (e.g., current RSI, ATR, or a custom metric), creating a new label on every bar wastes the object budget. The efficient pattern is to create the label once using var, then update its text and position on every bar using label.set_text(), label.set_xy(), and other setter functions. This consumes exactly one label slot regardless of how many bars have elapsed.

🔽 [Click to expand] View Full Pine Script Code — Single Updating Debug Label
//@version=6
indicator("Live Debug Label", overlay = false)

// ── RSI Calculation ──────────────────────────────────────────────────────────
int   rsiLen = input.int(14, title = "RSI Length", minval = 1)
float rsiVal = ta.rsi(close, rsiLen)   // RSI series value

// ── Create the label ONCE on bar 0 ──────────────────────────────────────────
// var ensures label.new() is called only on the very first bar.
// The returned label reference is stored and reused on every subsequent bar.
var label debugLabel = label.new(
     x         = bar_index,
     y         = rsiVal,
     text      = "",                    // Will be set dynamically
     color     = color.new(color.navy, 20),
     textcolor = color.white,
     style     = label.style_label_left,  // Pointer points left; label body is to the right
     size      = size.normal
)

// ── Update the label on EVERY bar ────────────────────────────────────────────
// label.set_xy() repositions the label to the current bar and RSI value.
label.set_xy(debugLabel, bar_index, rsiVal)

// label.set_text() updates the displayed text with the current RSI reading.
// str.tostring() converts the float to a string with 2 decimal places.
label.set_text(debugLabel, "RSI: " + str.tostring(rsiVal, "#.##"))

// Dynamically color the label based on RSI zone.
label.set_color(
     debugLabel,
     rsiVal >= 70 ? color.new(color.red, 10)   :   // Overbought zone
     rsiVal <= 30 ? color.new(color.green, 10) :   // Oversold zone
     color.new(color.gray, 20)                     // Neutral zone
)

// ── Plot RSI for reference ────────────────────────────────────────────────────
plot(rsiVal, title = "RSI", color = color.blue, linewidth = 1)
hline(70, "Overbought", color = color.red,   linestyle = hline.style_dashed)
hline(30, "Oversold",   color = color.green, linestyle = hline.style_dashed)
hline(50, "Midline",    color = color.gray,  linestyle = hline.style_dotted)

8. Pattern C — Multi-Line Tooltip for Rich Debug Data

The tooltip parameter accepts a series string and is displayed only on mouse hover, keeping the chart clean while embedding rich diagnostic information. This is particularly useful during development when you need to inspect multiple computed values at a specific bar without cluttering the visual output.

🔽 [Click to expand] View Full Pine Script Code — Tooltip Debug Labels
//@version=6
indicator("Tooltip Debug Labels", overlay = true, max_labels_count = 100)

// ── Indicators ───────────────────────────────────────────────────────────────
float atrVal  = ta.atr(14)                    // Average True Range
float rsiVal  = ta.rsi(close, 14)             // RSI
float volRatio = volume / ta.sma(volume, 20)  // Volume ratio vs 20-bar average

// ── Signal: High-volume RSI extremes ─────────────────────────────────────────
bool highVolSpike = volRatio > 2.0            // Volume more than 2x average
bool rsiOverbought = rsiVal >= 70
bool rsiOversold   = rsiVal <= 30

// ── Place a compact marker with a rich tooltip ────────────────────────────────
if highVolSpike and rsiOverbought
    // Build a multi-line tooltip string using newline characters.
    string ttText =
         "⚠ High-Vol Overbought\n" +
         "RSI   : " + str.tostring(rsiVal,   "#.##") + "\n" +
         "ATR   : " + str.tostring(atrVal,   "#.####") + "\n" +
         "VolRatio: " + str.tostring(volRatio, "#.##") + "x"

    label.new(
         x       = bar_index,
         y       = high,
         text    = "!",                        // Minimal visible text
         yloc    = yloc.abovebar,
         color   = color.new(color.red, 0),
         textcolor = color.white,
         style   = label.style_circle,         // Compact circle marker
         size    = size.tiny,
         tooltip = ttText                      // Rich data on hover
    )

if highVolSpike and rsiOversold
    string ttText =
         "✅ High-Vol Oversold\n" +
         "RSI   : " + str.tostring(rsiVal,   "#.##") + "\n" +
         "ATR   : " + str.tostring(atrVal,   "#.####") + "\n" +
         "VolRatio: " + str.tostring(volRatio, "#.##") + "x"

    label.new(
         x       = bar_index,
         y       = low,
         text    = "!",
         yloc    = yloc.belowbar,
         color   = color.new(color.green, 0),
         textcolor = color.white,
         style   = label.style_circle,
         size    = size.tiny,
         tooltip = ttText
    )

9. UDT History Access with Labels

When storing label references inside a User-Defined Type (UDT), accessing historical field values requires the correct Pine Script v6 syntax. Accessing myObj.field[1] is illegal; the correct form is (myObj[1]).field. Always guard against na on early bars:

🔽 [Click to expand] View Full Pine Script Code
//@version=6
indicator("UDT Label History Example", overlay = true)

// ── Define a UDT that holds a label reference ─────────────────────────────────
type SignalRecord
    label  lbl        // label reference field
    float  signalPrice

// ── Create a record on every bar (simplified example) ────────────────────────
var SignalRecord lastRecord = na

if bar_index % 10 == 0   // Every 10 bars for demonstration
    SignalRecord rec = SignalRecord.new(
         lbl         = label.new(
                            x     = bar_index,
                            y     = close,
                            text  = str.tostring(bar_index),
                            color = color.new(color.purple, 20),
                            textcolor = color.white,
                            style = label.style_label_center,
                            size  = size.tiny
                       ),
         signalPrice = close
    )
    lastRecord := rec

// ── Access historical UDT field — CORRECT v6 syntax ──────────────────────────
// ✗ WRONG:  lastRecord.signalPrice[1]
// ✓ CORRECT: (lastRecord[1]).signalPrice
float prevSignalPrice = na(lastRecord[1]) ? na : (lastRecord[1]).signalPrice
plot(prevSignalPrice, title = "Prev Signal Price", color = color.orange, style = plot.style_circles)

10. label.delete() — Explicit Cleanup

While Pine Script's garbage collector handles label cleanup automatically when the limit is reached, you can explicitly delete a label using label.delete(). This is useful when a label becomes logically obsolete before the limit is hit — for example, deleting a "pending" label once a condition resolves.

🔽 [Click to expand] View Full Pine Script Code
//@version=6
indicator("Label Delete Example", overlay = true)

// Store the most recent label reference
var label pendingLabel = na

bool newSignal = ta.crossover(ta.ema(close, 9), ta.ema(close, 21))
bool cancelSignal = ta.crossunder(close, ta.ema(close, 21))

if newSignal
    // Delete the previous pending label before creating a new one
    label.delete(pendingLabel)
    pendingLabel := label.new(
         x     = bar_index,
         y     = low,
         text  = "Pending",
         yloc  = yloc.belowbar,
         color = color.new(color.blue, 20),
         textcolor = color.white,
         style = label.style_label_up,
         size  = size.small
    )

if cancelSignal
    // Explicitly remove the label when the signal is invalidated
    label.delete(pendingLabel)
    pendingLabel := na   // Reset the reference to na

11. Key Mathematical Relationship: Label Count vs. Bar Count

Understanding the label budget mathematically helps design efficient scripts. If a label is created with probability $p$ per bar and the chart has $N$ bars, the expected number of labels created is:

$$E[\text{labels}] = p \times N$$

With the default limit of 50 labels, only the most recent 50 are visible. To ensure labels from the last $k$ bars are always visible, the signal frequency must satisfy:

$$p \leq \frac{50}{k}$$

For example, to guarantee labels are visible over the last 500 bars, the signal must fire no more than $p = 50/500 = 0.10$ (10%) of bars — or you must raise max_labels_count accordingly (up to 500).

12. Setter Functions Quick Reference

After creating a label, its properties can be modified using the following setter functions (all verified in the v6 Reference Manual):

Function What It Updates
label.set_xy(id, x, y)Repositions the label to a new bar index and price
label.set_x(id, x)Updates only the x (bar index) position
label.set_y(id, y)Updates only the y (price) position
label.set_text(id, text)Updates the displayed text string
label.set_color(id, color)Updates the background color
label.set_textcolor(id, color)Updates the text color
label.set_style(id, style)Updates the label shape style
label.set_size(id, size)Updates the label size
label.set_tooltip(id, tooltip)Updates the hover tooltip text
label.set_yloc(id, yloc)Updates the y-location mode
label.delete(id)Permanently removes the label object

Conclusion

  • 🔑 label.new() creates one label object per call; use conditional blocks to control when labels are created, and use var + setter functions to update a single persistent label efficiently.
  • 📦 The default label limit is 50 (not 500); raise it to a maximum of 500 via max_labels_count in indicator() or strategy(). Exceeding the limit triggers automatic garbage collection of the oldest labels.
  • 🛠 The tooltip parameter is the cleanest way to embed rich debug data — it keeps the chart visually minimal while exposing full diagnostic information on hover, making it ideal for development and production monitoring alike.

Ideas for Further Development

  1. Dynamic Label Table: Combine label.new() with a table object to create a hybrid display — use labels for per-bar annotations and a table for aggregate statistics (win rate, average ATR at signal, etc.) in a fixed corner of the chart.
  2. Label-Based Backtesting Audit Trail: In a strategy() script, create a label at every entry and exit with the tooltip containing the full trade rationale (entry price, stop distance, R-multiple target). This creates a visual audit trail that survives chart reloads and can be reviewed without opening the Strategy Tester.

Related Posts

Comments

Popular posts from this blog

Pine Script v6: indicator() vs strategy() — Core Functional Differences Explained

Pine Script v6 plot() Function: Complete Guide to Lines, Histograms, and Circles

Pine Script Variable Declaration: Mastering var, varip, and Proper Initialization Across Historical Bars