Overview

Pine Script v6 (released November 2024) is the current version, and TradingView ships new language features to v6 only; v5 scripts still run. Pick the script kind first, declare it once with explicit limits, and keep repainting rules in pine-script-security and backtest rules in pine-script-strategies. Every example here starts with //@version=6.

Pick the script kind by what it outputs

Use indicator() for anything that draws or alerts, strategy() only when you need simulated fills and a Strategy Tester report, and library() for functions shared across scripts.

  • Keep signal logic in an indicator or library and call it from the strategy. An indicator is cheaper to compute than a strategy.
  • Libraries export functions, methods, user-defined types, and enums. Exported functions cannot call input.*(), and a library cannot create alerts itself.

Set limits in the declaration and compute with built-ins

Declare overlay, max_lines_count, max_labels_count, and max_boxes_count once, at the minimum you need. Drawing objects default to 50 per type and cap at 500; past the cap the oldest objects vanish silently. A script may use at most 64 plot counts (plot, bgcolor, and similar all count) and 40 unique request.*() calls (64 on the Ultimate plan).

//@version=6
indicator("EMA Ribbon", overlay = true, max_lines_count = 50)
 
fastLen = input.int(9, "Fast EMA", minval = 1, maxval = 200)
slowLen = input.int(21, "Slow EMA", minval = 1, maxval = 200)
 
fastEMA = ta.ema(close, fastLen)
slowEMA = ta.ema(close, slowLen)
p1 = plot(fastEMA, "Fast EMA", color.blue)
p2 = plot(slowEMA, "Slow EMA", color.orange)
fill(p1, p2, fastEMA > slowEMA ? color.new(color.green, 85) : color.new(color.red, 85))

Give every input a title and minval/maxval bounds; the Settings dialog is the script’s API. Prefer ta.* built-ins over for loops, because a single loop is limited to 500 ms per bar. Recycle drawing objects (line.set_*, line.delete) instead of creating one per bar.

Use var for state that persists across bars

A plain assignment re-runs on every bar; var initializes once and keeps its value. Use varip only for state that must survive intrabar ticks on a realtime bar.

var int upStreak = 0
upStreak := close > close[1] ? upStreak + 1 : 0

Wire alerts to the right function

Use alertcondition() in indicators and alert() in strategies or when the message must be dynamic. A strategy cannot create alerts from alertcondition() calls.

crossUp = ta.crossover(fastEMA, slowEMA)
alertcondition(crossUp, "Bullish cross", "Fast crossed above slow on {{ticker}} {{interval}}")
if crossUp
    alert("Bullish cross at " + str.tostring(close), alert.freq_once_per_bar_close)

alert() defaults to alert.freq_once_per_bar, which fires intrabar and can fire on a signal that disappears before the bar closes. Pass alert.freq_once_per_bar_close for signals. Alerts only trigger on realtime bars, so history cannot confirm an alert condition.

Publish libraries with typed parameters and an exact version

Type every parameter of an exported function and import a specific version number. TradingView offers no “latest” import; each published update increments the version, and consumers must edit their import line to adopt it.

//@version=6
// @description ATR-based sizing helpers
library("VolUtils")
 
// @function ATR stop distance in price units
// @param atrLen ATR period
// @param mult ATR multiplier
export atrStop(simple int atrLen, simple float mult) =>
    ta.atr(atrLen) * mult
//@version=6
indicator("Stop distance", overlay = true)
import yourUsername/VolUtils/1 as vol
plot(close - vol.atrStop(14, 2.0))
  • Return several values as a tuple ([upper, mid, lower]) and destructure them at the call site.
  • Use simple int for length parameters. Built-ins such as ta.atr expect a length that does not change per bar.
  • A library must be published (private is allowed) before another script can import it. The @function, @param, and @returns annotations are optional and feed the hover documentation.
  • Pine has no unit-test framework. Test a library with a companion indicator that plots each exported function on an instrument with known values.

Know the v6 behavior changes when migrating

Booleans can no longer be na, numbers no longer cast to bool implicitly (use bool()), and and/or evaluate lazily. Move calls that need history on every bar out of conditional expressions into the global scope. Dividing two int values now yields a fractional result; wrap with int() to truncate. timeframe.period always includes a multiplier ("1D", not "D"). The transp parameter is gone; use color.new(color, transparency).