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_up | Bubble with pointer pointing upward (default for buy signals) |
label.style_label_down | Bubble with pointer pointing downward (default for sell signals) |
label.style_label_left | Bubble with pointer pointing left |
label.style_label_right | Bubble with pointer pointing right |
label.style_label_center | Bubble with no pointer, centered |
label.style_circle | Circular shape |
label.style_diamond | Diamond shape |
label.style_flag | Flag shape |
label.style_arrowup | Upward arrow (no text bubble) |
label.style_arrowdown | Downward arrow (no text bubble) |
label.style_xcross | X-cross marker |
label.style_cross | Plus-cross marker |
label.style_square | Square shape |
label.style_triangleup | Upward triangle |
label.style_triangledown | Downward 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.
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_countinindicator()orstrategy(). Exceeding the limit triggers automatic garbage collection of the oldest labels. - 🛠 The
tooltipparameter 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
- Dynamic Label Table: Combine
label.new()with atableobject 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. - 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.
Comments
Post a Comment