Overview

Pine Script v5 runs inside TradingView and recompiles on every bar. The runtime is unusual: series-typed values, a global bar_index, and a security-call model that repaints if used carelessly. This page covers the conventions that produce stable, non-repainting scripts. Read general-principles first.

Pick the right script kind

Pine has three script kinds. Choose by what the script needs to do, not by familiarity.

  • indicator(...): visual output on the chart. Plots, fills, alerts. No trading. Cheap to compute.
  • strategy(...): backtestable trading logic with strategy.entry, strategy.exit, strategy.close. TradingView simulates fills and tracks PnL.
  • library(...): reusable functions imported by other scripts via import user/name/1. No plots, no strategy.*, no UI inputs.

A script that calculates a signal and a script that trades the signal should be separate. Keep the indicator clean and call it from a strategy script via request.security on the same symbol, or copy the function into a library.

Use var for state that survives across bars

A plain assignment re-runs on every bar. var initializes once and persists.

//@version=5
indicator("Streak", overlay=false)
 
var int upStreak = 0
upStreak := close > close[1] ? upStreak + 1 : 0
plot(upStreak)

Use varip only when you need state that updates intrabar on every tick and survives bar close; the rare correct use is a live order-book accumulator. Most strategies should never see varip.

Avoid lookahead in request.security

The default request.security call repaints. To get a stable historical value that matches what live execution sees, request the previous bar of the higher timeframe with lookahead_off (the default) and a [1] offset.

htfClose = request.security(syminfo.tickerid, "60", close[1], lookahead=barmerge.lookahead_off)

Never set lookahead=barmerge.lookahead_on in a strategy that simulates fills, or backtests will look magic and live trading will not. The only legitimate lookahead_on use is in an indicator that explicitly studies future-bar relationships for research, and you label the plot accordingly.

Plot performance: budget your draws

Each script has a hard limit on plot, line, label, and box objects. Hit the limit and TradingView silently drops the oldest objects, which looks like a bug.

  • Cap max_lines_count, max_labels_count, max_boxes_count in the indicator(...) declaration based on what you actually need.
  • Delete objects you no longer need with line.delete, label.delete, box.delete.
  • Avoid creating a new object on every bar when an xloc.bar_index-anchored update is enough.

A script that draws a 200-bar history of pivots should keep a fixed array of line handles and recycle them, not allocate one per bar.

Strategy entries and exits

Use the dedicated strategy calls; do not reinvent fills with if blocks and global counters.

//@version=5
strategy("Donchian", overlay=true, initial_capital=10000, default_qty_type=strategy.percent_of_equity, default_qty_value=10)
 
length = input.int(20)
upper = ta.highest(high, length)
lower = ta.lowest(low, length)
 
if close > upper[1]
    strategy.entry("long", strategy.long)
if close < lower[1]
    strategy.close("long")

Use strategy.entry for signaled entries (it reverses an opposite position automatically), strategy.order for unconditional orders, and strategy.exit for bracket exits with stop and target. Reference [1] when comparing against the prior bar, so the signal is computed on a closed bar.

Alerts with alertcondition and alert

alertcondition declares an alert the user wires in the TradingView UI; it compiles into the script’s available alert list. alert fires at runtime when called and supports dynamic messages.

crossUp = ta.crossover(close, ta.sma(close, 50))
alertcondition(crossUp, title="Cross up", message="Close crossed above SMA(50)")
 
if crossUp
    alert("Cross up at " + str.tostring(close), alert.freq_once_per_bar_close)

Use alert.freq_once_per_bar_close for signal alerts; intrabar firing repaints alerts the same way it repaints plots. Mention the timeframe and symbol in the message body so a phone-delivered alert is self-describing.

Inputs are the contract

Every magic number gets an input.int, input.float, input.bool, or input.source with a clear title and a sensible default. The Settings dialog is the user’s only configuration surface; treat it like an API.

For shared math (RSI variants, volatility filters, position sizers), publish a library and import it from the indicators and strategies that use it. Versioned libraries avoid the copy-paste drift that the same dependency rules in general-principles warn against, and the typed-input discipline mirrors the validation patterns in typescript.