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 withstrategy.entry,strategy.exit,strategy.close. TradingView simulates fills and tracks PnL.library(...): reusable functions imported by other scripts viaimport user/name/1. No plots, nostrategy.*, 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_countin theindicator(...)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.