---
title: "Pine Script: Best Practices"
slug: "pine-script"
category: "coding"
tags: ["coding", "pine-script", "tradingview"]
status: "stable"
last_updated: 2026-08-14
summary: "Pine v6 conventions: script kinds, var state, request.security without repainting, plot performance, and alert wiring."
related: ["[[coding/general-principles]]", "[[coding/typescript]]", "[[coding/python]]"]
---

## Overview

Pine Script v6 (shipped November 2024, current as of 2026) 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. v6 tightened the type system: booleans can no longer be `na`, `and`/`or` evaluate lazily, and technical-analysis calls must use the `ta.*` namespace (`ta.ema`, not bare `ema`). Convert v5 scripts with the Pine Editor's "Convert code to v6" command, then fix what the converter flags. This page covers the conventions that produce stable, non-repainting scripts. Read [[coding/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.

```pine
//@version=6
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.

```pine
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.

```pine
//@version=6
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.

```pine
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 [[coding/general-principles]] warn against, and the typed-input discipline mirrors the validation patterns in [[coding/typescript]].

## Related

- [[coding/general-principles]]
- [[coding/typescript]]
- [[coding/python]]
